REST APIs allow various clients, including browsers, desktop apps, mobile apps, and any device with an internet connection, to communicate with a server. So it’s very important to design REST APIs properly so that we don’t run into problems in the future.
To create a robust API, there are many details and factors that be considered, from basic security to using the right HTTP methods, implementing authentication, and deciding which requests and responses are accepted and returned, among many others.
So let’s look at some practical recommendations that make a good API. All tips are language-independent, so they apply to any framework or technology. So let’s get down to business.
- Prefer to use nouns in endpoint paths
We should consider using nouns that represent the entity we are retrieving or manipulating as the path name and always favor using plural designations. Avoid using verbs in endpoint paths because our HTTP request method already has the verb that does not add new information.
The action must be indicated by the HTTP request method we are making. The most common methods are GET, POST, PATCH, PUT and DELETE.
– GET retrieves resources.
– POST creates a new resource on the server.
– PUT/PATCH updates an existing resource.
– DELETE removes the resource
Verbs map to CRUD operations.
With these principles in mind, we should create routes like GET /books to get a list of books, not GET /get-books or GET /book. Likewise, POST /books add a new book, PUT /books/{id} updates the complete data of the book with a given id, while PATCH /books/{id} makes partial book changes. Finally, DELETE /books/{id} deletes an existing book with the given ID.
It is not forbidden to use verbs or nouns in the singular; a way to please everyone would be to set the following rule: “Be consistent with the choice you made”
So if you use the names in the singular, do that throughout your API, and be consistent.
- JSON is the primary format for sending and receiving data
Accepting and responding to API requests was done in XML until a few years ago. But nowadays, JSON (JavaScript Object Notation) has become the “default” format for sending and receiving API data in most applications. Therefore, the recommendation is to ensure that our endpoints return JSON data format as a response and also, when accepting information through the HTTP message payload.
While Form Data is fine for sending customer data, especially if we want to send files, it’s not ideal for text and numbers. We don’t need form data to pass it like most frameworks. We can pass JSON directly on the client side.
When receiving data from the client, we need to ensure that the client interprets the JSON data correctly and to do this, the Content-Type in the response header must be set to application/json when making the request.
The exception is worth mentioning again if we are trying to send and receive files between the client and server. For this specific case, we need to handle file responses and send form data from the client to the server.
- Use a set of predictable HTTP status codes
It’s always a good idea to use HTTP status codes according to your definitions to indicate the success or failure of a request. Use the same status codes for the same results across the API. Some examples are:
200 for overall success
201 for a successful creation
400 for invalid client requests such as invalid parameters
401 for unauthorized requests
403 for missing permissions on resources
404 for missing resources
429 for many orders
5xx for internal errors (these should be avoided as much as possible)
There may be more status codes depending on your use case, but limiting the number of status codes helps the client to consume a more predictable API.
- Return standardized messages
In addition to using HTTP status codes that indicate the result of the request, always use standardized responses for similar endpoints. Consumers can always expect the same structure and act accordingly. This applies to success messages and error messages as well.
In the case of fetching collections, keep a specific format in case the response body includes an array of data like this:
[
{
bookId: 1,
name: "O alienista"
},
{
bookId: 2,
name: "A coisa"
}
]
Or a combined object like this:
{
"data": [
{
bookId: 1,
name: "O alienista"
},
{
bookId: 2,
name: "A coisa"
}
],
"totalDocs": 200,
"nextPageId": 3
}
The advice is to be consistent no matter what approach you choose to do. The same behavior should be implemented when fetching an object and also when creating and updating resources, for which it is generally a good idea to return the last instance of the object.
While not harmful, it is redundant to include a generic message like “Book created successfully” as this is implicit in the HTTP status code.
Lastly, error codes are even more important when you have a standard response format. This message should include information that a customer can use to present errors to the end user, not a generic alert like “Something went wrong” that we should avoid as much as possible. Here’s an example:
{
“code”: “book/not_found”,
“message”: “Could not find a book with ID 6”
}
Again, it’s not necessary to include the status code in the response content, but it’s useful to define a set of error codes like book/not_found so that the consumer maps them to different strings and decides its error message for the user.
In particular, for development environments, it may seem appropriate to include the error stack in the response to help with debugging.
- Use paging, filtering, and sorting when searching collections of records
Once we build an endpoint that returns a list of items, pagination should be put in place. Collections are usually quick, so it’s important always to return a limited and controlled amount of elements.
It’s fair to allow API consumers to choose how many objects to get, but it’s always a good idea to predefine a number and have a maximum quantity. The main reason is that it will consume a lot of time and bandwidth to return a huge range of data.
There are two well-known ways to implement paging: skip/limit or using a keyset.
The first option allows for a more user-friendly way of fetching data but is generally less performant as databases will have to scan a lot of documents when fetching “bottom line” records. On the other hand, paging using a keyset takes an identifier/id as a reference to “cut” a collection or table with a condition without scanning records.
Along the same lines of thought, APIs should provide filters and sorting features that enrich the data obtained. Database indexes are part of the solution to improve and maximize performance with the access patterns applied through these filters and sorting options.
As part of the API design, these paging, filtering, and sorting properties are defined as query parameters in the URL. For example, if we wanted to get the top 10 books that belong to a “novel” category, our endpoint would look like this:
GET /books?limit=10&category=novel
- Consider using PATCH instead of PUT
It is unlikely that we will ever need to update the whole record. There are often sensitive or complex data that we want to keep out of user manipulation.
With that in mind, to perform partial updates to a resource should be used the PATCH requests, whereas PUT replaces an existing resource entirely. Both must use the request body to pass the information to be updated.
PATCH is the recommendation to modify a specific field, while PUT requests is use to modify the complete object. However, it’s worth mentioning that nothing prevents us from using PUT for partial updates. There are no “network transfer restrictions” validating this, just a convention that it’s a good idea to follow.
- Provide extended answer options
Access patterns are critical when designing what API resources are available and what data is the return. As a system grows, registry properties also grow, but not all of these properties are always required for clients to operate.
In these situations, that provide the ability to return shortened or full responses to the same endpoint becomes useful. If the consumer only needs a few fields, having a simplified response helps reduce bandwidth consumption and potentially the complexity of fetching other calculated fields.
An easy way to approach this feature is to provide an extra query parameter to enable/disable providing the extended response.
GET /books/:id
{
“bookId”: 1,
“name”: “O Alienista”
}
GET /books/:id?extended=true
{
“bookId”: 1,
“name”: “O Alienista”
“tags”: [“tale”, “novel”],
“author”: {
“id”: 1,
“nAme”: “Machado de Assins”
}
- Endpoint responsibility
The Single Responsibility Principle (SRP): the concept of keeping a function, method, or class focused on a narrow behavior and doing it well. When we think of an API, we can say that a good API is one thing and never changes.
This helps consumers better understand our API and make it predictable, which makes overall integration easier. To be more complete, extending our endpoints available list is better, rather than creating very complex endpoints that try to solve many things at once.
- Provide good documentation for the API
Consumers of your API should be able to understand how to use it and what to expect from the available endpoints. This is only possible with good and detailed documentation. Consider the following aspects to provide a well-documented API.
– Available endpoints describing their purpose;
– Permissions required to run an endpoint;
– Examples of invocation and response;
– Error messages to expect;
The other important part of making this a success is the documentation up to date after system changes and additions. The best way to achieve this is to make API documentation a key part of development.
Two well-known tools in this regard are Swagger and Postman, which are available for most existing API development frameworks.
- Use SSL for security and configure CORS
Security is another fundamental property that our API must have. Configuring SSL by installing a valid certificate on the server will ensure secure communication with consumers and prevent many potential attacks.
CORS (Cross-Origin Resource Sharing) is a browser security feature that restricts cross-origin HTTP requests initiated from scripts running in the browser. If your REST API resources receive cross-origin non-simple HTTP requests, you need to enable CORS support for consumers to operate accordingly.
The CORS protocol requires the browser to send a preflight request to the server and wait for approval (or a prompt for credentials) from the server before sending the actual request. The preflight request appears in the API as an HTTP request that uses the OPTIONS method (among other headers).
Therefore, to support CORS, a REST API resource needs to implement an OPTIONS method that can respond to the OPTIONS mock request with at least the following response headers required by the Fetch pattern:
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Allow-Origin
What values to assign these keys will depend on how open and flexible we want our API to be. We can assign specific methods and known origins or use wildcards to have open CORS constraints.
- API version
Endpoints begin to change and be rebuilt as part of the development evolution process. But we should avoid, as much as possible, the sudden change of endpoints for consumers. It’s a good idea to think of the API as a backward-compatible, where new and updated endpoints be made available without affecting previous standards.
This is where API versioning comes in handy, where clients should be able to select which version to connect to. There are several ways to declare API versioning:
- Add a new header “x-version=v2”
- Have a query parameter “?apiVersion=2”
- Make the version part of the URL: “/v2/books/:id”
Going into detail about which approach is more convenient when to make a new version official, and when to discontinue old versions are interesting questions to ask. Obtain extra information on API versioning in the article: Versioning your API.
- Use data caching to improve performance
To help our API performance, it’s beneficial to know what data that rarely changes and is accessed frequently. We can consider this type of data to use an in-memory database or cache that prevents access to the main database.
The main challenge with this approach is that the data can become out of date, so a process for implementing the latest version must also be considered.
Using cached data will be useful for consumers to load configurations and catalogs of information that don’t change frequently. When using caching, be sure to include the Cache-Control information in the headers. This will help users to effectively use the caching system.
- Use standard UTC dates
At the data level, it’s important to be consistent in how dates are displayed to client applications.
ISO 8601 is the international standard format for date and time-related data. Dates must be in “Z” or UTC format, from which customers can decide a time zone. If this date needs to be displayed under any conditions, here is an example:
{
“createdAt”: “2022-03-08T19:15:08Z”
}
- Provide a health check endpoint
There may be difficult times when our API is down, and it may take some time to get it up and running. In these circumstances, customers would like to know that services are not available so that they can be aware of the situation and act accordingly.
To achieve this, provide an endpoint (such as GET /check) that determines whether or not the API is healthy. This endpoint may be called by other applications such as load balancers. We might even go a step further and report maintenance periods or health conditions on parts of the API. To achieve this, provide an endpoint (such as GET /check) that determines whether or not the API is healthy.
- Accept API key authentication
Enabling authentication via API keys provides the ability for third-party applications to easily integrate with our API.
These API keys must be passed using a custom HTTP header (such as Api-Key or X-Api-Key). Keys must have an expiration date and must be revoked so they can be invalidated for security reasons.
That’s all.
*The content of this article is the author’s responsibility and does not necessarily reflect the opinion of iMasters.
Leave a comment