Motivation
This article is written primarily for backend developers that are looking for some practical examples of "how-to" design their REST APIs so they would be strain forward for other developers as well as for API consumers.
The anatomy of the end-point path
HTTP method
Every endpoint belongs to the HTTP method. Those methods give developers and users a basic understanding of "what action" is performed on resources on that path. It's required to use the proper HTTP method for each endpoint. The details about each of the listed below methods can be found in RFC2616:
-
GET
should be used if the endpoint returns information about the given resource ("list items", "get the item with ID=5", "get all subitems of the item with ID=5" and so on); -
POST
should be used if the endpoint creates a resource during request or should somehow change the state of the resources ("create new item", "perform authentication"); -
PUT
is used when there is a need to completely replace a resource with an updated version. It's mostly used for update operations ("update item"); -
PATCH
is similar toPUT
but mostly is usable when you want to indicate, that the resource can be partly updated ("change user status to active", "grant user a permission/access"); -
DELETE
as the name says, it indicates that the endpoint performs deletion of resource ("delete item", "delete all items");
Other HTTPS methods are less common to use and you probably will know if you need to use them.
Endpoint path
As we already know, the HTTP method is a verb, so to describe the endpoint path we need to use nouns (in other words "domain names").
For example to create a user we write:
POST /users // GOOD
POST /create-user // BAD
We've already specified POST
as a method so we know, "it's going to create a user".
Another example to update user status:
PATCH /users/status // GOOD
PATCH /users/set-status // BAD do not use verbs in paths
PUT /users/status // BAD use proper HTTP method
Most of the time you will face CRUD routes with some additional end-points to work with sub-entities:
GET /users // Get list of users
GET /users/${userID} // Get single user details
POST /users // Create a new user
PUT /users/${userID} // Update user
DELETE /users/${userID} // Delete user
PATCH /users/access // Partly update user
GET /users/${userID}/photos // Get sub-entity
POST /users/${userID}/photos // Create sub-entity
DELETE /users/${userID}/photos/${photoID} // Delete sub-entity
Query parameters
Often you need to specify additional parameters for pagination or some kind of filtering that is provided by your server.
Pagination
If you're not using pagination on end-points that return lists of items, you probably should, because the growth of the database request would last a long time and suddenly block your server or database from running. The user also won't be happy to wait 15 seconds for items he doesn't want to see in numbers.
Example of query string with pagination:
GET /users?page=1&pageSize=25 // "Classic" pagination
GET /users?fromId=1232142 // Cursor pagination
Filtering
In case you need to specify some additional search parameters or return only specific entity fields you will also add them into query string and parse on the server-side:
GET /users?search=John // Search for user with name John
GET /users?status=active,banned&age=18-21,22-27,40-49 // Return only active or banned users within the specified age groups. If you want to specify few filters you separate them by ","
GET /users?online=2021-12-01,2022-01-01 // Fetch users that were online in range of dates
Sorting
Users usually want to see "recent" items or updates, but sometimes they want to apply other sorting options:
GET /users?sort=last_online // Sort by last online ASC
GET /users?sort=last_online,status // Sort by 2 fields
GET /users?sort=name&desc=true // Sort by name in descending order
GET /users?sort=+name,-status // Multisort with specifying "+"/"-" as ASC/DESC
Response HTTP status codes
Depending on the result of the request the server should return a proper status code to indicate if the request was successfully finished or there were errors and it can't be finished.
It's also a good practice to use the defined set of codes for all end-points and provide additional messages within a response or in the documentation.
Those status codes are described at RFC 2616, RFC 4918, RFC 6585 and others.
Most of the time, you would be using those status codes:
2xx
200 OK
- Simply means that the request was successfully performed and resource is available in response.
201 Created
- Mostly used in POST
requests as an indication that resource was successfully created and stored in the server.
204 No Content
- Mostly used in DELETE
requests to indicate that resource doesn't exist anymore.
4xx:
400 Bad Request
- the request doesn't satisfy validation rules and the server denies processing it. Addition details about errors can be specified in the response body.
401 Unautorized
- user is not authorized to use this end-point. Most of the time this is a status code to use if the user session is timed out or access token/session token was not provided in the Authorization header or within a cookie.
403 Forbidden
- if it's a response to a sing-in end-point request it simply means that "there is no user with such username & password combination" or that user has no right to perform the request.
404 Not Found
- the resource doesn't exist or there is no end-point on this address. The additional message about the error should be provided in the response body.
409 Conflict
- mostly used when performing the request is impossible due to a constraint on the server (for example user with the specified nickname already exists) or if the given entity was already modified before the user sent a request.
422 Unprocessable Entity
- means that the request schema is correct and it passes validation rules, but the server can't process the request or work with that query.
429 Too Many Requests
- simply means that the user sent too many requests to the server (for example if the user tries to log in too many times in 1 minute).
5xx
500 Internal Server Error
- your server should have a handler for unexpected errors and send a response to the user if there is something wrong before shutting the server down.
Conclusions
It's important to use things as they were designed to be used and to provide an interface that would be intuitive and easy to work with.
Also, don't forget to document your APIs using popular instruments like Swagger. It's helpful when you want to provide some explanations over "what does this status code mean" or "how to use filters in this end-point".
You will get some experience only by doing things. With all the basic rules you should be fine until you will come to some specific cases.
Top comments (9)
How do I design a API for when I want to perform some action in the Backend, for ex I want to run some business logic which will validate if a user is eligible to place an order (ex, if he has cleared his past due balance), how should we expose an API for such use cases.
It depends.
If you want user to instantly get notified about the status of the request and there is no heavy computation happening - you probably want to perform an request to validate a user and then to perform another request to check if user was validated and can perform other actions.
Doing things separately and being able to check it from another place is equally important.
Thanks, but how would we design it to make it RESTful as in how would we avoid having the endpoint as a verb than a noun, and if we use a noun what would be the right entity or model to use.
Great post I actually needed this since I'm just getting into API Development.
API development is a pure fun! It may look as a simple thing but at the same time it can be challenging. Good luck and enjoy!
saved me moments before the interview when I needed some revision! thank you !
I'm glad that it was helpful!
I use JSend as standard for API response from server.
Thank you for your comment.
JSend is a good thing if we're talking about handling response body. So it's a nice addition for other good solutions.