15 fundamental tips on REST API design

15 fundamental tips on REST API design

Thanks to the original writer and article :

https://medium.com/@liams_o/15-fundamental-tips-on-rest-api-design-9a05bcd42920


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 browsers, 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 number of things that need to be in place. From basic security to using the right HTTP methods, implementing authentication, and deciding which requests and responses are accepted and returned among many others. In this post, I’m trying my best to compress into 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 be 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 add any new information.


The action should be indicated by the HTTP request method that we’re making. The most common methods are 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 is 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 making 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's worth mentioning 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 the client to the server.

3. Use a set of predictable HTTP status codes

It is always a good idea to use?HTTP status codes?according to their 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 on 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, the usage of HTTP status codes that indicate the outcome of the request always uses standardized responses for similar endpoints. Consumers can always expect the same structure and act accordingly. This also applies to success but also error messages. In the case of fetching collections stick to particular format whether 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 of 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 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?in the response to help debug bugs. But please do not include those in production as it’d create a security risk by 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 over time 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 a good idea to predefine a number and have a maximum for it. The main reason for this is that it will be very time and bandwidth-consuming to return a huge array of data.

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 in 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 is worth to mention nothing stops us from using?PUT?for partial updates, there are 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 this situation where providing the ability to return reduced or full responses for the same endpoint becomes useful. If consumers only needs 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 it and what to expect from the available endpoints. This is only possible with 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 in 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 is another fundamental property that our API should have. Setting up SSL by installing a valid certificate on the server will ensure 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 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?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 possible suddenly changing endpoints for consumers. It is a good idea to think of 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 do 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 are not meant to change much over time. 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 a date needs to be displayed under any conditions. Here’s an example ofz how dates should look like:

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

14. A health check endpoint

There might be rough times when our API is down and it might take some time to get it up and running. Under these 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-Key or?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.

Happy Learning!

Kamran Sarwar

Senior Technical Lead @ Siemens | Golang | PHP | Node.js | JavaScript | Kubernetes | Docker | Gitlab, CI/CD | Kafka, MQTT, IoT | Building Backend Services And Managing Deployments

2 年

Doing a great job. Keep sharing ...

要查看或添加评论,请登录

社区洞察

其他会员也浏览了