In the previous episode we talked about core concepts of web API designs, including resources and representations, naming, relations, functions and sanity checks. We looked at what should be the expected behavior of HTTP methods regarding operations on single objects and collections.
Today we will focus expand on the topic of collections, namely: filtering, sorting and pagination. While talking about filtering collections, we will look at filtering fields in particular single objects. In order to do that we will need a set of options to customize HTTP request.
Today we are mainly interested in how to pass additional information along with GET request, but of course this applies also to other HTTP methods. There are several ways to do that.
The most obvious seem to be to include a parameter in the URL directly like this: /cars/911. It’s not exactly parametrization, but an identification of particular resource. Everything that is a part of URL up to the “?” character is a part of unique name of resource and is mandatory.
When operating on collections we typically use query parameter, so everything that’s after the “?” character, like this: /cars?color=red. Multiple parameters are separated with “&” character.
When we want to pass more general parameter, not related to particular endpoint, we can use one of the standard headers, like “Range” or create a custom header. Custom headers have a convention to start with x-[company name]-, for example x-amz-storage-class in AWS storage services or x-ms-version in Microsoft versioning. Using headers makes API surface cleaner but might be a bit harder to handle correctly by clients.
HTTP specifications permits GET method to have a body, but it should not have any meaning. If we are considering using a body due to large number to parameters, or nesting of parameters, it might be a sign that we need POST method instead of GET. For example, performing a search might then create a search object that can later be referenced by unique URL.
We can specify that we want to obtain object with particular properties using query parameters like this: /cars?color=red. What about different operators than equality? We might use operators like: “in”, “nin”, “neg”, “gt”, “gte”, “lt” and “lte” followed by separator, for example “:” and the value to compare against, for example: /cars?color=in:red,green,blue for car colors, or: /users?age=gte:18 that would mean users with age greater or equal 18. Alternatively we can glue the operator to property like this: /users?ageGreaterThanEquals=18. As a convenience we might predefine some searches, for example a ticket system could handle query like this: /tickets?page=recentlyClosed.
After filtering out the set of objects interesting to us, we might also consider filtering out a content of the objects itself. We can do that be specifying list of fields we want to keep in the object like this: /users?fields=name,title,age. On the other hand, if we want to get all fields except some, we can do it like this: /users?exclude=resume,biography. We can predefine a set of object profiles that contain various subset of data and refer to them like this: /users?style=compact.
In this articles series we focus on REST approach, there are however interesting alternatives, like the GraphQL – an API query language used internally by Facebook and released in 2015. It allows defining precisely what data is need both at collection, object level, and can save us multiple requests.
After filtering the interesting results, we would like to order them properly.
If we want to sort in ascending order by single field, we can go by simple:
One way to specify order is to add “+asc” or “+desc” suffix to field name:
We can use similar, but more compact and elegant version with “+” and “-“ prefixes:
Alternatively, we can have list of fields for sorting and parameters “asc” and “desc” that specify which fields should be sorted which way.
Or, a separate parameter for each field denoting the direction.
My favorite would be “+” and “-“ prefixes.
After filtering and sorting, we need to be able to specify a subset of resulting collection. We can do that using the “limit” parameter and either “offset” or “page”. It is helpful to include in the response links to next and previous page as well as first, last and self – the current one. However, offset and page lead to problem of what happens when the collection is modified between subsequent requests. In order to solve this, we can use “cursor” or “marker” parameter that will point to the first object on given page, and navigate via next and previous links. Instead of using query parameters, we can go with Accept-range header but it doesn’t seem to be very popular choice.
In the following episode, we will take a look at constructing a proper error responses and various HTTP status codes. Stay tuned.