15 fundamental tips on REST API design

Williams O
10 min readApr 29, 2022

--

REST APIs are one of the most common types of web services available, but they’re also hard to design. They allow various clients including browser, desktop apps, mobile applications and basically any device with an internet connection, to communicate with a server. Therefore, it’s very important to design REST APIs properly so that we don’t run into problems down the road.

Creating an API from scratch can be overwhelming due to the amount of things that need to be in place. From basic security to using the right HTTP methods, implementing authentication, deciding which requests and responses are accepted and returned among many others. In this post, I’m trying my best to compress in 15 items some powerful recommendations on what makes a good API. All tips are language-agnostic, so they potentially apply to any framework or technology.

1. Make sure to use nouns in endpoint paths

We should always use the nouns that represent the entity that we’re retrieving or manipulating as the pathname and always in favor of using plural designations. Avoid using verbs in the endpoint paths because our HTTP request method already has the verb and doesn’t really adds any new information.

The action should be indicated by the HTTP request method that we’re making. The most common methods being GET, POST, PATCH, PUT, and DELETE.

  • GET retrieves resources.
  • POST submits new data to the server.
  • PUT/PATCH updates existing data.
  • DELETE removes data.

The verbs map to CRUD operations.

With these principles in mind, we should create routes like GET /books for getting a list of books and not GET /get-books nor GET /book. Likewise, POST /books is for adding a new book , PUT /books/:id is for updating the full book data with a given id, while PATCH /books/:idupdates partial changes on the book. Finally, DELETE /books/:id is for deleting an existing article with the given ID.

2. JSON as the main format for sending and receiving data

Accepting and responding to API requests were done mostly in XML until some years back. But these days, JSON (JavaScript Object Notation) has largely become the “standard” format for sending and receiving API data in most applications. So our second item recommends to make sure that our endpoints return JSON data format as a response and also when accepting information through the payload of HTTP messages.

While Form Data is good for sending data from the client, especially if we want to send files, is not ideal for text and numbers. We don’t need Form Data to transfer those since with most frameworks we can transfer JSON directly on the client side. When receiving data from the client, we need to ensure the client interprets JSON data correctly, and for this the Content-Type type in the response header should be set to application/json while making the request.

It worth to mention once again the exception if we’re trying to send and receive files between client and server. For this particular case we need to handle file responses and send form data from client to server.

3. Use a set of predictable HTTP status codes

It is always a good idea to use HTTP status codes according to its definitions to indicate the success or failure of a request. Don’t use too many, and use the same status codes for the same outcomes across the API. Some examples are:

  • 200 for general success
  • 201 for successful creation
  • 400 for bad requests from the client like invalid parameters
  • 401 for unauthorized requests
  • 403 for missing permissions onto the resources
  • 404 for missing resources
  • 429 for too many requests
  • 5xx for internal errors (these should be avoided as much as possible)

There might be more depending on your use case, but limiting the amount of status code helps the client to consume a more predictable API.

4. Return standardized messages

In addition to the usage of HTTP status codes that indicate the outcome of the request always use a standardized responses for similar endpoints. Consumers can always expect the same structure and act accordingly. This also applies for success but also error messages. In the case of fetching collections stick to a particular format wether the response body includes an array of data like this:

[
{
bookId: 1,
name: "The Republic"
},
{
bookId: 2,
name: "Animal Farm"
}
]

or a combined object like this:

{
"data": [
{
"bookId": 1,
"name": "The Republic"
},
{
"bookId": 2,
"name": "Animal Farm"
}
],
"totalDocs": 200,
"nextPageId": 3
}

The advice is to be consistent regardless the approach you choose for this. The same behavior should be implemented when fetching an object and also when creating and updating resources to which is usually a good idea to return the last instance of the object.

 // Response after successfully calling POST /books
{
"bookId": 3,
"name": "Brave New World"
}

Although it won’t hurt, it is redundant to include a generic message like “Book successfully created” as that is implied from the HTTP status code.

Last but not least, error codes are even more important when having a standard response format. This message should include information that a consumer client can use to present errors to the end user accordingly a not a generic “Something went wrong” alert which we should avoid as much as possible. Here’s an example:

{
"code": "book/not_found",
"message": "A book with the ID 6 could not be found"
}

Again, it is not necessary to include the status code in the response content but it is useful to define a set of error codes like book/not_found in order for the consumer to map them to different strings and decide its own error message for the user. In particular for Development / Staging environments it might seem adequate to also include the error stack to the response to help debugging bugs. But please do not include those in production as it’d create a security risk exposing unpredictable information.

5. Use pagination, filtering and sorting when fetching collections of records

As soon as we build an endpoint that returns a list of items pagination should be put in place. Collections usually grow overtime thus it is important to always return a limited and controlled amount of elements. It is fair to let API consumers choose how many objects to get but is always good idea to predefine a number and have a maximum for it. The main reason for this being that it will be very time and bandwidth consuming to return a huge array of data.

To implement pagination, there are two well known ways to do it: skip/limit or keyset. The first option allows a more user friendly way to fetch data but is usually less performant as databases will have to scan many documents when fetching “bottom line” records. On the other hand, and the one I prefer, keyset pagination receives an identifier/id as the reference to “cut” a collection or table with a condition without scanning records.

In the same line of thinking, APIs should provide filters and sorting capabilities that enrich how data is obtained. In order to improve performance, database indexes take part of the solution to maximize performance with the access patterns that are applied through these filters and sorting options.

As part of the API design these properties of pagination, filtering and sorting are defined as query parameters on the URL. For instance if we want to obtain the first 10 books that belong to a “romance” category, our endpoint would look like this:

GET /books?limit=10&category=romance

6. PATCH instead of PUT

It is very unlikely that we have a need to fully update a complete record at once, there’s usually sensitive or complex data that we want to keep out from user manipulation. With this in mind,PATCH requests should be used to perform partial updates to a resource, whereas PUT replaces an existing resource entirely. Both should use the request body to pass in the information to be updated. Only modified fields in the case of PATCH and the full object for PUT requests. Nonetheless it worth to mention nothing stop us from using PUT for partial updates, there’s no “network transfer restrictions” that validate this, is just a convention that is a good idea to stick to.

7. Provide extended response options

Access patterns are key when creating the available API resources and which data is returned. When a system grows, record properties grow as well in that process but not all of those properties are always required for clients to operate. It is in these situation where providing the ability to return reduced or full responses for the same endpoint becomes useful. If consumer only need some basic fields, having a simplified response helps to reduce bandwidth consumption and potentially the complexity o fetching other calculated fields.

An easy way to approach this feature is by providing an extra query parameter to enable/disable the provision of the extended response.

GET /books/:id
{
"bookId": 1,
"name": "The Republic"
}
GET /books/:id?extended=true
{
"bookId": 1,
"name": "The Republic"
"tags": ["philosophy", "history", "Greece"],
"author": {
"id": 1,
"name": "Plato"
}
}

8. Endpoint Responsibility

The Single Responsibility Principle focuses on the concept of keeping a function, method, or class, focused on a narrow behavior that it does well. When we think about a given API, we can say it is a good API if it does one thing and never changes. This helps consumers to better understand our API and make it predictable which facilitates the overall integration. It is preferable to extend our list of available endpoints to be more in total rather than building very complex endpoints that try to solve many things at the same time.

9. Provide Accurate API Documentation

Consumers of your API should be able to understand how to use and what to expect from the available endpoints. This is only posible with a good and detailed documentation. Have into consideration the following aspects to provide a well documented API.

  • Endpoints available describing the purpose of them
  • Permissions required to execute an endpoint
  • Examples of invocation and response
  • Error messages to expect

The other important part for this to be a success is to have the documentation always up to date following the system changes and addition. The best way to achieve this is to make API documentation a fundamental piece of the development. Two well known tools on this regard are Swagger and Postman which are available for most of the API development frameworks out there.

10. Use SSL for Security and configure CORS

Security, another fundamental property that our API should have. Setting up SSL by installing a valid certificate on the server will ensure a secure communication with consumers and prevent several potential attacks.

CORS (Cross-origin resource sharing) is a browser security feature that restricts cross-origin HTTP requests that are initiated from scripts running in the browser. If your REST API’s resources receive non-simple cross-origin 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 request for credentials) from the server before sending the actual request. The preflight request appears into 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 preflight request with at least the following response headers mandated by the Fetch standard:

  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers
  • Access-Control-Allow-Origin

Which values to assign to 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 restrictions.

11. Version the API

As part of the development evolution process, endpoints start to change and get rebuilt. But we should avoid as much as posible suddenly changing endpoints for consumers. It is a good idea to think the API as a backwards compatible resource where new and updated endpoints should become available without affecting previous standards.

Here’s where API versioning becomes useful where clients should be able to select which version to connect to. There are multiple ways to declare API versioning:

1. Adding a new header "x-version=v2"
2. Having a query parameter "?apiVersion=2"
3. Making the version part of the URL: "/v2/books/:id"

Getting into the details on which approach is more convenient, when to make official a new version and when to deprecate old versions are certainly interesting questions to ask, but to not extend this item in excess that analysis will be part of another post.

12. Cache data to improve performance

In order to help the performance of our API it is beneficial to keep an eye on data that rarely changes and is frequently accessed. For this type of data we can consider using an in-memory or caché database that saves from accessing the main database. The main challenge with this approach is that data might get outdated thus a process to put the latest version in place should be considered as well.

Using cached data will become useful for consumers to load configurations and catalogs of information that is not meant to change many overtime. When using caching make sure to include Cache-Control information in the headers. This will help users effectively use the caching system.

13. Use standard UTC dates

I cannot think of a system’s reality that doesn’t work with dates at some point. At the data level it is important to be consistent on how dates are displayed for client applications. The ISO 8601 is the international standard format for date and time related data. The dates should be in “Z” or UTC format from which then clients can decide a timezone for it in case such date needs to be displayed under any conditions. Here’s an example on how dates should look like:

{
"createdAt": "2022-03-08T19:15:08Z"
}

14. A health check endpoint

There might be rough times where our API is down and it might take some time to get it up and running. Under this circumstances clients will like to know that services are not available so they can be aware of the situation and act accordingly. In order to achieve this, provide an endpoint (like GET /health) that determines whether or not the API is healthy. This endpoint can be called by other applications such as load balancers. We can even take this one step further and inform about maintenance periods or health conditions on parts of the API.

15. Accept API key authentication

Allowing authentication via API Keys offers the ability for third party applications to easily create an integration with our API. These API keys should be passed using a custom HTTP header (such as Api-Keyor X-Api-Key). Keys should have an expiration date, and it must be possible to revoke them so that they can be invalidated for security reasons.

Thanks for reading! 📖

You can follow me on Twitter for more posts and cool stuff.

Cheers! 🤘

--

--