Share 15 basic tips about REST API design

13bbd888f916074175b485919495c8c9.jpeg

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

Translation | Yang Xiaoai

REST APIs are one of the most common types of web services, but they can also be difficult to design. They allow clients of all kinds, including browsers, desktop applications, mobile applications, and basically any device with an internet connection, to communicate with a server. So it's very important to design the REST API properly so that we don't run into problems in the future.

Creating an API from scratch can be overwhelming due to the amount of things that need to be prepared. From basic security to using the correct HTTP methods, implementing authentication, deciding which requests and responses to accept and return, and more. 

In this article, I'll do my best to condense some strong recommendations on what constitutes a good API into 15 that can help you as a reference in future development projects. All hints are language-independent, so they may apply to any framework or technology.

1. Make sure to use nouns in the endpoint path

We should always use nouns for pathnames that denote the entities we are retrieving or manipulating, and plural names are always supported. Avoid using verbs in the endpoint path, since our HTTP request method already has verbs and doesn't really add any new information.

The action should be indicated by the HTTP request method we are crafting. The most common methods are GET, POST, PATCH, PUT, and DELETE.

  • GET retrieves a resource.

  • POST submits new data to the server.

  • PUT/PATCH updates existing data.

  • DELETE deletes data.

Verbs map to CRUD operations.

With these principles in mind, instead of GET /get-books or GET /book , we should create a route like GET /books to get a list of books.

Likewise, POST /books is used to add a new book, PUT /books/:id is used to update the complete book data with a given id, and PATCH /books/:id updates the book with partial changes. 

Finally, DELETE /books/:id is used to delete an existing article with a given ID.

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

A few years ago, accepting and responding to API requests was mostly done in XML. But today, JSON (JavaScript Object Notation) is largely the "standard" format for sending and receiving API data in most applications. 

Therefore, our second recommendation ensures that our endpoints return JSON data format in response, and also when accepting information via the payload of an HTTP message.

While Form Data is great for sending data from clients, especially if we want to send files, it's not ideal for text and numbers. We don't need form data to transfer them, because with most frameworks we can transfer JSON directly on the client side. 

When receiving data from the client, we need to ensure that the client interprets the JSON data correctly. To do this, the Content-Type type in the response header should be set to application/json when making the request.

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

3. Use a predictable set of HTTP status codes

It's always a good idea to use HTTP status codes by definition to indicate the success or failure of a request. Don't use too much, and use the same status code for the same result throughout the API. Some examples are:

  • 200 means general success 

  • 201 means the creation was successful

  • 400 Bad request from client, such as invalid parameters

  • 401 Unauthorized Request

  • 403 lack of permission on the resource

  • 404 Missing resource

  • 429 too many requests

  • 5xx indicates an internal error (these should be avoided if possible)

Depending on your use case, there may be more, but limiting the number of status codes helps clients use a more predictable API.

4. Return standardized messages

Always use standardized responses for similar endpoints, in addition to using HTTP status codes that indicate the result of the request. Consumers can always expect the same structure and act accordingly. This also works for success, but also for error messages. In the case of fetching a collection, stick to a specific format, whether the response body contains an array of data like this:

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

or a composite object like this:

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

No matter which method you choose for this, the advice is the same. You should achieve the same behavior when getting an object as when creating and updating a resource, it's usually a good idea to return the last instance of an object.

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

While it wouldn't hurt, including a generic message like "Book successfully created" is redundant since it's implied from the HTTP status code.

Last but not least, error codes are more important when they have a standard response format. 

This message should contain information that the consumer client can use to display an error to the end user, rather than the generic "something went wrong" alert that we should avoid whenever possible. Here is an example:

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

Again, it is not necessary to include a status code in the response content, but it is useful to define a set of error codes (like book/not_found) so that consumers can map them to different strings and decide their own error messages for users. Especially for For development/staging environments, it seems sufficient to also include the error stack into the response to help debug errors. 

But please don't include this in a production environment as it creates a security risk of exposing unpredictable information.

5. Use pagination, filtering and sorting when fetching record collections

Once we build an endpoint that returns a list of items, pagination should happen. Collections typically grow over time, so it's important to always return a finite and controlled number of elements.

It's fair to let the API consumer choose how many objects to fetch, but it's always a good idea to predefine a number and set a maximum for it. The main reason for this is that returning large amounts of data would be very time and bandwidth consuming.

To implement paging, there are two well-known methods: skip/limit or keyset.

The first option allows data to be fetched in a more user-friendly manner, but is generally less performant because the database has to scan many documents when fetching "bottom line" records. 

On the other hand, what I prefer is that keyset pagination receives an identifier/id as a reference to "cut" a collection or table without scanning the records.

Along the same lines, the API should provide filter and sorting capabilities to enrich the way data is obtained. To improve performance, database indexes become part of the solution to maximize performance for the access patterns applied through these filters and sort options.

As part of the API design, these properties for paging, filtering, and sorting are defined as query parameters on the URL. For example, if we wanted to get the top 10 books belonging to the "romance" category, our endpoint would look like this:

GET /books?limit=10&category=romance

6. Use PATCH instead of PUT

It's unlikely that we'll need to update complete records all at once, often there will be sensitive or complex data that we want to avoid user action. With this in mind, PATCH requests should be used to perform partial updates to resources, while PUT completely replace existing resources. 

Both should use the request body to pass the information to be updated. For full objects for PATCH and PUT requests, only fields are modified.

Still, it's worth mentioning that there's nothing stopping us from using PUT for partial updates, there's no "network transfer limit" to verify this, it's just a convention worth sticking to.

7. Provide extended response options

Access patterns are key in creating available API resources and what data is returned. As the system grows, record attributes grow in the process, but not all of these attributes are always required for client operation. 

It is in these cases that it becomes useful to provide the ability to return a reduced or full response for the same endpoint. If the consumer only needs some basic fields, simplifying the response can help reduce bandwidth consumption and possibly reduce the complexity of fetching other computed fields.

An easy way to achieve this is to provide an additional query parameter to enable/disable the serving of extended responses.

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 Responsibilities

The Single Responsibility Principle focuses on preserving the notion of a function, method, or class, focusing on the narrowly defined behavior it excels at. When we consider a given API, we can say it is a good API if it does only one thing and never changes. 

This helps consumers better understand our API and makes it predictable, which facilitates overall integration. It is better to expand our list of available endpoints to more than to build very complex endpoints trying to solve many problems at the same time.

9. Provide Accurate API Documentation

Consumers of the API should be able to understand how 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.

  • The available endpoints describe their purpose

  • Permissions required to execute the endpoint

  • Call and Response Example

  • expected error message

Another important part of being successful is always keeping documentation up to date after system changes and additions. The best way to achieve this is to make API documentation an essential part of development. Two well-known tools for this are Swagger and Postman, which are available for most API development frameworks.

10. Secure with SSL and configure CORS

Security, another fundamental property 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 from scripts running in the browser. 

If your REST API's resources receive non-trivial 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 the server's approval (or request for credentials) before sending the actual request. 

Preflight requests appear in the API as HTTP requests using the OPTIONS method (among other headers). 

Therefore, to support CORS, REST API resources need to implement an OPTIONS method that can respond to OPTIONS preflight requests with at least the following response headers as dictated by the Fetch standard:

  • Access Control Allow Method

  • Access-Control-Allow header

  • Access-Control-Allow-Origin

The values ​​assigned 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 open up CORS restrictions.

11. API version

As part of the development evolution process, endpoints began to change and rebuild. But we should try to avoid abruptly changing the consumer's endpoint as much as possible. 

It's a good idea to think of the API as a resource for backwards compatibility, where new and updated endpoints should be available without affecting previous standards.

This is where API versioning becomes useful, clients should be able to choose which version to connect to. There are various 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"

Knowing in detail which approach is more convenient, when the new version will be officially released and when the old one will be deprecated are certainly interesting questions to ask, but don't overextend this project, the analysis will be part of another article.

12. Cache data to improve performance

To improve the performance of our API, it is beneficial to keep an eye on data that rarely changes and is accessed frequently. For this type of data, we can consider using an in-memory or cache database, thus avoiding access to the main database. 

The main challenge with this approach is that the data can become outdated, so an up-to-date process should also be considered.

Using cached data is useful for consumers to load configuration and information catalogs that do not change over time. 

When using caching, make sure to include Cache-Control information in the header. This will help users to use the caching system efficiently.

13. Use standard UTC dates

I can't think of a reality of a system that doesn't work with dates at some point. At the data level, it is important to be consistent in how dates are displayed by client applications.

ISO 8601 is an international standard format for date and time related data. Dates should be in 'Z' or UTC format according to which the client can decide the time zone for them in case such dates need to be displayed in any case. Here's an example of what the date should look like:

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

14. Health check endpoint

Our API can fail in hard times, and it can take some time to get it up and running. In this case, customers will want to know that the service is unavailable so they can understand and act accordingly. To achieve this, provide an endpoint such as GET /health to determine if the API is healthy. This endpoint can be called by other applications, such as load balancers. We can even go a step further and notify about maintenance periods or health of parts of the API.

15. Accept API key authentication

Allowing authentication via an API key provides third-party applications with the ability to easily create integrations with our API.

These API keys should be passed using custom HTTP headers 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 become invalid for security reasons.

Summarize

In today's article, I share with you 15 basic tips about API design. I hope these tips are helpful to you. If you find it useful, please remember to like, I follow me, and post this article Share it with your developer friends, maybe it will help them.

Finally, thanks for reading.

learn more skills

Please click the public number below

f8e734bfad7b2c0090a0cd2ee8f6168f.gif

Guess you like

Origin blog.csdn.net/Ed7zgeE9X/article/details/130417642