Approach 3: requests and responses
In previous approaches, I told you how the idea to summarize the experience in web API development came to my mind. In the first approach, I described the kinds of resources and operations that we meet during web API design. The second part was dedicated to unique URLs allowing to address these resources. In this part I will try to describe various requests and responses.
Universal response
I have already mentioned that the format of communication between a client and a server may vary depending on the developer’s preferences. As for me, JSON is the handiest and most descriptive, but a real app may support several formats. Now let’s focus on the structure and the necessary attributes of a response object. That’s right, we will wrap all the data returned by the server in a special container - a universal response object that will contain all the necessary service information for further processing. Let’s take a look at it.
Success — marker of request processing success
In order to understand immediately whether the request was successful, a developer should just use “success” marker. The simplest server response without any additional data will look like this:
POST /api/v1/articles/22/publish
{
“success”: true
}
Error — information about errors
If a request failed - possible reasons and a variety of server messages will be discussed later - a response gets an attribute “error” containing an http-code and error message. Don’t mess it with validation errors for different fields! I think that the most convenient way is to return a status code in a response heading, but I’ve seen a different approach where the heading always contains status 200 “success” and the details and data of possible errors are contained in the body of the response.
GET /api/v1/user
{
“success”: false,
“error”: {
“code” : 401,
“message” : ”Authorization failed”
}
}
Data — data, returned by server
The majority of server responses are to return some data. An expected data set may vary depending on the request type and its success, but the ‘data’ attribute will be present in the majority of responses.
Here is an example of returned data, where the response contains the requested “user” object.
GET /api/v1/user
{
“success”: true,
“data”: {
“id” : 125,
“email” : ”john.smith@somedomain.com”,
“name” : ”John”,
“surname” : ”Smith”,
}
}
And here we have an error report - the names of fields and messages about validation errors.
PUT /api/v1/user
{
“success”: false,
“error”: {
“code” : 422,
“message” : ”Validation failed”
}
“data”: {
“email” : ”Email could not be blank.”,
}
}
Pagination — information for navigation between pages
Beyond the elements collection, returned in a response, there must also be the information about pagination.
The minimal values set for pagination consists of:
- a number of entries;
- a number of pages;
- a current page number;
- a number of entries per page;
- a maximal number of entries supported on the server side.
Some web API developers also add a set of links to the next and the previous pages as well as the first, the last and the current.
GET /api/v1/articles
Response:
{
“success”: true,
“data”: [
{
“id” : 1,
“title” : ”Interesting thing”,
},
{
“id” : 2,
“title” : ”Boring text”,
}
],
“pagination”: {
“totalRecords” : 2,
“totalPages” : 1,
“currentPage” : 1,
“perPage” : 20,
“maxPerPage” : 100,
}
}
Errors correction
As I have already told, not all requests to web API are successful, but it is also a part of a game. Informing about an error is a powerful tool that can greatly simplify the client work and guide a client app to the right way. The word “error” is not the best in this case. I prefer calling these cases “exception” as a request was successfully received, processed and the app returned an adequate answer explaining why the request can’t be fulfilled.
So what are possible reasons for exceptions?
500 Internal server error — oops, it’s broken, but we’re already fixing it
In this case, a problem occurred on the server side and the client app can only give a sigh and inform a user that the server got tired and decided to have a break. For example, database connection has been lost or there are bugs in the code.
400 Bad request — now it’s you who’s broken something
This response is opposite to the previous one. It is returned when a client app sends a request that cannot be processed due to syntax errors or the lack of mandatory parameters . Usually it’s cured by rereading web API documentation.
401 Unauthorized — stranger, who are you?
Authorization is required to address this resource. Of course, it’s not a guarantee of the resource availability, but without it, you can’t proceed any further. It can appear in case of addressing a closed API part or expiration of the current token.
403 Forbidden — you’re not allowed here
The requested resource exists, but a user is not authorized to view or edit it.
404 Not found — nobody home
There are 3 major reasons for this response: the route is incorrect, a resource has been deleted or a user is not authorized to know that resource exists. For example, while you were surfing a goods list one of them got old-fashioned and was deleted.
405 Method not allowed — don’t act like that!
This exemption is related to the used verb (GET, PUT, POST, DELETE) that indicates an action that we are trying to perform. If the requested resource does not support this action, a server will inform about it.
422 Unprocessable entity — fix and send again
One of the most useful exemptions, which is returned each time if data in a request contains some logic errors. Here data means either a set of parameters and values sent by the GET method, or object fields, passed in a request body by POST, PUT and DELETE methods. If data is not validated, the server returns information of invalid parameters and the reasons of invalidity in the ‘data’ section.
HTTP protocol supports much more different status-codes for all the possible cases, but they are rarely used and of little practical value in the web API context. As far as I remember I didn’t use any exemptions except the ones I listed above.
Requests
My personal classification of requests is the following:
1. Requests to collections
- get elements of a collection (GET)
- add a new element to a collection (POST)
- address a property of a collection (GET)
- request a function of a collection (POST)
2. Requests to object
- get an object (GET)
- get a property of an object (GET)
- edit an object (PUT/PATCH)
- edit a property of an object (PUT/PATCH)
- delete an object (DELETE)
- set or delete relationships (as a special case of a function) (POST)
- request other functions of an object (POST)
3. Work with files
- upload a file on a server (POST)
- get a file (GET)
Get elements of collection
One of the most popular requests is getting elements of a collection. News feed, goods lists, information and statistics tables and many others are displayed by an application with the help of collections. In order to perform this request, we address a collection, using GET method and adding additional parameters in the request line. As we said above, we expect to get an array of similar elements and information for pagination — upload of the further list or page. The content can be limited and sorted with the help of additional parameters that we’ll discuss later.
Pagination
The page parameter indicates which page should be displayed. If this parameter isn’t specified, the app will display the first page. The very first success response from the server will specify, how many pages the collection counts within the current parameters. If the value exceeds the max number of pages it would be reasonable to return an exemption 404 Not found.
GET /api/v1/news?page=1
perPage indicates the desired number of elements on a page. Usually API has a default value returning the value of perpage in pagination, but sometimes it allows to enlarge this value setting a max value in maxPerPage:
GET /api/v1/news?perPage=100
Sorting
The sampling results often should be sorted in ascending or descending order of values of the fields supporting quantitative (for numbers) or alphabetical (for strings) sorting. For example, we want to sort the users list by name or goods by price. Besides that we can set sorting from A to Z and vice versa and moreover, make it different, for different fields.
sortBy — there are several ways to transfer data of complex sorting in GET parameters. Here we need to specify the order and direction of sorting.
In some API it can be done as a string:
GET /api/v1/products?sortBy=name.desc,price.asc
Some other variants propose passing an array:
GET /api/v1/products?
sortBy[0][field]=name&
sortBy[0][direction]=desc&
sortBy[1][field]=price&
sortBy[1][direction]=asc
Both options are equal, as they pass the same instructions. As for me, arrays are more universal, but you know, tastes differ...
Simple filtration by value
Usually to filter sorting by a given parameter, it is enough to indicate the name of the field and the required value. For example, we want to filter articles by author ID:
GET /api/v1/articles?authorId=25
Complex filters
In some interfaces we need a more complex system of filtering and search. Let’s take a look at the most popular ones.
Filters by top and bottom values using comparisons operators from (higher or equal), higher, to (lower or equal), lower. It is applied to the fields whose values can be ranged.
GET /api/v1/products?price[from]=500&price[to]=1000
Filters by several values from the list. It is applied to the fields that have a limited number of possible values, for example, results can be filtered by several statuses.
GET /api/v1/products?status[]=1&status[]=2
Filters by a partial match of a string. It is applied to the fields containing tests or information that can be considered as a text like articles of goods, phone numbers, etc.
GET /api/v1/users?name[like]=John
GET /api/v1/products?code[like]=123
Named filters
Sometimes when a set of filtering parameters is used frequently and can be considered as a whole, especially when they are related to internal, often complicated sampling methods, it is reasonable to group them in a form of named filters. In this case all what you need is to pass a filter name in your request and the system will create sampling automatically. GET /api/v1/products?filters[]=recommended Named filters can also have their proper parameters. GET /api/v1/products?filters[recommended]=kidds
Getting objects
To receive an exact object, you should use its unique address. In the previous articles I told you that objects may be divided into 2 groups: elements of collections (similar entities having the same properties but different IDs) and unique elements (unique in terms of the whole app like global settings or in terms of a current user or session like profile settings). That means that in order to address the first we use a collection address and ID: GET /api/v1/articles/25 For access to the second, we have a unique route:
GET /api/v1/myProfile
In both cases we receive a set of properties of a requested object and their values that usually are scalar (strings, numbers, boolean types).
GET /api/v1/articles/25
Response:
{
“success”: true,
“data”: {
“id” : 25,
“createdAt” : “2017-01-01 12:45”,
“title” : ”Cool article”,
“text” : ”This is a very interesting thing...”
}
}
Managing a set of returned fields
I have often seen examples of web API where a request for an object had necessary fields as parameters. I would like to remind that we’re talking not about a flat data storage like classical RESTFull API, but a more complex and designed for custom tasks web API where the server side plays the main role. In other words, the server side knows better which data should be returned to the client and has full information about the purposes for which they can be used. In real life development of the server and the client side is often performed in parallel, so managing a set of fields is acceptable only as a rare exemption required for very special tasks.
Related entities, lists and aggregated fields
It often happens that for correct work a client app needs not only an object, but also a range of related objects: it’s more comfortable to get an article together with an author and latest comments, and have comments related to authors. Each related object or list is returned in a pseudo field, which is not a property of an object, but an indication of relationship. Therefore, requesting an object we receive a considerable hierarchical structure that contains single objects like a user as well as lists of objects. The needed nesting is discussed “before setting a sail” while designing interactions between the client app and the server side. Besides related entities and lists, properties of an object are complemented by aggregated values indicating, for example, the number of relationships, and used for receiving data per request.
In the case, described above, we’ll get the following result:
GET /api/v1/articles/25
Response:
{
“success”: true,
“data”: {
“id” : 25,
“title” : ”Cool article”,
“text” : ”This is a very interesting thing...”,
“userId”: 10,
“user” : {
“id” : 10,
“name” : “John Smith”,
},
“totalComments” : 17,
“recentComments” : [
{
“id” : 40,
“text” : ”It is great!”,
“userId”: 20,
“user”: {
“id” : 21,
“name” : “Michael Black”,
},
}
]
}
}
All the related lists that can potentially be very large (it’s true about almost all related lists), must be limited on the server side. The major part of work with related lists should be performed by calling collection resources.
In this approach I tried to tell you about the most popular ways of getting a required sampling. Probably you can share much more examples and details regarding this subject. I will be glad if you complete my post. Anyway, it’s already quite large, so other types of requests will be described in the next parts.