REST-API Best Practices

  • Post by Dennys Fredericci
  • Dec 14, 2021

The most used web service nowadays is REST APIs. It is essential to design it properly to avoid problems in the future, following the three pillars of a good API: security, performance, and ease of usage.

If we don’t follow these three pillars, there is a chance to create problems for API consumers and maintainers.

So, we will look to the best practices to be easy to understand, consuming, secure and fast.

1. Use JSON

In the past, accepting and responding to API requests were done on XML, but these days JSON(JavaScript Object Notation) is the standard format for transferring data.

We can make sure that client will interpret it as JSON setting the Content-Type in the response header to application/json. Many frameworks and libraries already use this header information to parse the payload correctly.

However, if we transfer files between client and server, we need a different Content-Type to tell clients that it is a file and not a JSON.

2. Use nouns instead of verbs in endpoint paths

The HTTP protocol already has the verbs, called the request method. Having verbs in our API paths is unnecessary since it provides no new information.

So, the action indicated by the HTTP request method should follow as described below:

GETRetrieve resources
POSTAdd new data to the server
PUTUpdate an existing data
DELETERemove data

With this in mind, let’s see all endpoint to manage the resource books:

GET/books
POST/books
PUT/books/:id
DELETE/books/:id

The PUT and DELETE paths have the parameter id since they need to know the exact book to update or delete.

You can see it as a mapping to CRUD operations.

3. Name collections with plural nouns

We have to use plural nouns because we deal with a collection of resources. Here is an example to clarify:

GET/booksRetrieve a collection of books
POST/booksAdd a new book to the books collection.
PUT/books/:idUpdate the book with id four from the books collection.
DELETE/books/:idRemove the book with id four from the books collection.
4. Nesting resources for hierarchical objects

We should group endpoints that contain associated information. If an object contains another object, we should design the endpoint to express it. It can be done even if our database is not structured like this.

It is also good to avoid mirroring our database structure in our endpoints to avoid giving the attacker unnecessary information.

Let’s see an example to retrieve reviews from a specific book.

GET /books/4/reviews

The endpoint above returns all reviews from the book where the id is four. It makes sense because a review doesn’t exist without a book.

Nesting resources can quickly go too far. If it happens, we can use hypermedia, especially if the data is not contained within the top-level object.

5. Maintain Good Security Practices (Use SSL for Security)

All communication between client and server must be private. Therefore the usage of SSL/TLS is mandatory.

There is no secret about installing and using SSL, and if you are afraid of the cost, there are many cheap solutions. Just check if this fits in your use case.

A client should not be able to access more information than they requested. For example, a user should not access another user or admin data.

6. Handle errors gracefully

We shouldn’t leave unhandled exceptions to force the API client to handle it. Instead, we have to handle errors gracefully and return the HTTP response codes to indicate what is happened with a determined request.

If we do it, we eliminate confusion for API clients when something goes wrong. Some standard HTTP status codes to return:

400 Bad RequestThis means that client-side input fails validation.
401 UnauthorizedThis means the user isn’t not authorized to access a resource. It usually returns when the user isn’t authenticated.
403 ForbiddenThis means the user is authenticated, but it’s not allowed to access a resource.
404 Not FoundThis indicates that a resource is not found.
500 Internalerror – This is a generic server error. It probably shouldn’t be thrown explicitly.
502 Bad GatewayThis indicates an invalid response from an upstream server.
503 Service UnavailableThis indicates that something unexpected happened on the server-side (It can be anything like server overload, some parts of the system failed, etc.).

There is also an RFC 7807(https://datatracker.ietf.org/doc/html/rfc7807), a simple specification that defines a JSON format with five optional attributes to describe a problem, and this should be the response body in case of errors.

Here is an example of 400 Bade Request:

100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
Content-Language: en

{
   "type":"https://example.net/validation-error",
   "title":"Your request parameters didn't validate.",
   "invalid-params":[
      {
         "name":"age",
         "reason":"must be a positive integer"
      },
      {
         "name":"color",
         "reason":"must be 'green', 'red' or 'blue'"
      }
   ]
}
7. Versioning our APIs

During the API life, the interface will change, and old clients should work even in case of a new version with a new interface. Due to this challenge, we have a version strategy. We can do it in the path of our endpoint or the header.

The most common approach implementing the versioning is adding a version in the path of our endpoint, like:

GET/v1/books
GET/v2/books
8. Caching

Caching is a known strategy by many of us to avoid fetching the data from the database for every request. This can be implemented using Redis, Memcached, Hazelcast, etc.

But when we are talking about REST APIs, we should also consider using the header Cache-Control.

The Cache-Control is an HTTP cache header that contains a set of parameters to define caching policies. The client should use this header to make calls to our API only when necessary.