Skip to main content

Restful API best practices

Restful API should be used to ensure communication between the client and server. While making a Restful API, you should model it with best practices in mind.

Data Format

JSON should be used as a format for sending and receiving data. To ensure the client interprets JSON data correctly, you should set the Content-Type: application/json; charset=utf-8 response header.

Versioning

The most effective way to evolve your API without breaking changes is to follow effective API change management principles. Straightforward solution is to set API version in URI path:

https://url/api/v1

Endpoint paths

Use kebab case and plural nouns instead of verbs in endpoint paths. For example, if we need CRUD for user model paths should look like this:

URLMethod
https://url/api/v1/usersGET
https://url/api/v1/users/:idGET
https://url/api/v1/usersPOST
https://url/api/v1/users/:idPATCH
https://url/api/v1/users/:idDELETE
https://url/api/v1/some-pathGET

To show relations between models/tables, use nesting on endpoints. For example, if one user has multiple notes, the endpoint should look like this https:/url/api/v1/users/:id/notes.

Avoid nesting more than three levels deep, as this will make your API less readable.

HTTP status codes

You should use regular HTTP status codes in your API responses, as this will help clients know whether the request is successful or not.

RangeReponse description
100 – 199Informational Responses.
200 – 299Successful Responses.
300 – 399Redirects
400 – 499Client-side errors
500 – 599Server-side errors

Filtering

The easiest and fastest way to do filtering is to do it on the backend by filtering data in the database. It can be done by providing query parameters in the endpoint that will be used as a filtering option. Use kebab case when naming query parameters.

Example: filter users by name → https://url/api/v1/users?some-query=value

Pagination

Implementing pagination on the backend API involves dividing the data into smaller, manageable chunks and allowing the client to retrieve these chunks as needed. It can be done in several ways.

The first one is to scroll items until all are listed. It is done in a way that the frontend provides the number of objects and the id of the last fetched object. The backend checks the number of fetched objects. If the number is equal to the number in the query param, the backend returns with fetched objects tag that there are more objects to fetch (eq. loadMore: true). Tag is false if the number of fetched objects lower than a limited number. In the database, it is easy to do with LIMIT OFFSET.

Example: https://URL/api/users?limit=10&last-id=20

Another way is to have numbered pagination on the frontend. Then frontend needs to send the number of items per page parameter and picked the page number. It is crucial that the backend on the first request sends a number of items in the database, so the frontend knows how many pages there will be. The rest of the logic handles the backend.

Example: https://url/api/v1/users?per-page=10&page=1

Example of numbered pagination API response
{
"data": [
{...},
{...},
...
],
"meta": {
"total": 100,
"pages": 10,
"perPage": 10,
"page": 1,
"next": true,
"prev": false
}
}

Documentation

To document your API, you should use Swagger. There are a few ways to generate a swagger.json file:

  • Writing and updating swagger.json by hand.
  • Dynamically generate swagger.json by writing jsdoc comments and using swagger-jsdoc.
  • Using a library that supports decorators like tsoa.

It is important to provide as much as possible info while writing controllers.

Testing

To ensure that the API behaves according to the defined specifications, it is recommended to write tests. For all future updates and features, by simply running tests, it's easy to check if the API behaves differently and thus find bugs early in development. Suggested libraries to use are jest and supertest.

Remember having both e2e and unit/integration tests is what you should be aiming for if your resources allow it. Having only e2e tests is always better than having no tests at all.

Authentification

JWT token is a tool that is an industry standard for authentication. Tokens are created on registration or login and sent to the frontend. The frontend should provide it as a Bearer token in every request where it is needed. With middleware, the token is decoded and data stored in it are accessible to the backend for usage. The most common cause is the user id is stored. Also, you can put the expiration time of tokens and other features that you can find on their official web.

Authorization

As mentioned, you can store any data inside the JWT token. It is usually data that is needed throughout the whole app, like language and roles. When JWT is decoded, you can extract all data inside from it. With simple middleware, you can check if the user has permission to send the wanted request.

Error handling

Error handling should be done through util classes, and errors should have descriptions that provide other services to know what happened. Errors can have custom messages, especially if errors will be shown as popup messages on frontend. If an error is unhandled, it will be passed to express middleware as the last step in error handling that will throw Something went wrong! and code 500.

Data validation

The proposal is to use ajv module. It is a simple way to define a JSON schema. This module is great because you can check if the request contains the required fields. This module validates JSON, so you can check data provided in the header, body, params, and query with a custom validator.