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:
GET | Retrieve resources | |||
POST | Add new data to the server | |||
PUT | Update an existing data | |||
DELETE | Remove 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 | /books | Retrieve a collection of books | ||||||
POST | /books | Add a new book to the books collection. | ||||||
PUT | /books/:id | Update the book with id four from the books collection. | ||||||
DELETE | /books/:id | Remove 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 Request | This means that client-side input fails validation. | ||
401 Unauthorized | This means the user isn’t not authorized to access a resource. It usually returns when the user isn’t authenticated. | ||
403 Forbidden | This means the user is authenticated, but it’s not allowed to access a resource. | ||
404 Not Found | This indicates that a resource is not found. | ||
500 Internal | error – This is a generic server error. It probably shouldn’t be thrown explicitly. | ||
502 Bad Gateway | This indicates an invalid response from an upstream server. | ||
503 Service Unavailable | This 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.