Don’t obsess over meaningless norms

Before I start this article, I’d like to say this: RESTful is really nice, but it’s just a software architecture style, and obsessing over how to conform to the specifications is just annoying and defeats the purpose of using it.

For example, the Elasticsearch API will pass JSON directly in the GET request, but this is a business requirement, because ordinary Query Param simply cannot construct such a complex Query DSL. Github’s V3 API also has a number of non-standard aspects, which does not prevent it from becoming the industry’s reference standard for RESTful apis.

Some of the things I’m going to cover are not up to standard, but these are the things I’ve come across, puzzled over, and thought about in actual development, so they are what I consider to be best practices for RESTful apis.

Why RESTful

The biggest thing I find about RESTful is that it’s normative, easy to understand, and elegant. A well-structured, easy-to-understand API can save a lot of pointless communication and documentation. And RESTful is becoming more and more popular, and there are more and more excellent peripheral tools (such as the documentation tool Swagger).

agreement

If the whole site HTTPS is of course the best, if not, please try to use HTTPS login, registration and other interfaces involving passwords.

version

The version number of the API has nothing to do with the version number of the client APP. Do not let the APP pass the version number they used to submit the APP market to the server. Instead, provide API version numbers like V1, v2, etc. Version numbers are only allowed to enumerate, not to determine ranges.

The version number can be concatenated in the URL or put in the Header. Such as:

api.xxx.com/v1/users
Copy the code

Or:

api.xxx.com/users

version=v1
Copy the code

request

In general, the external form of an API is nothing more than add, delete, change, and review (of course, the business logic is much more complex), and queries are divided into details and lists, which in RESTful terms are like generic templates. For example, when designing an API for an Article, the most basic urls are these:

  • GET /articles: List of articles
  • GET /articles/id: Article Details
  • POST /articles/: Create an article
  • PUT /articles/id: Revise the article
  • DELETE /articles/id: Delete the article

RESTful uses GET, POST, PUT, and DELETE to query, create, modify, and DELETE resources. Besides POST, the other three requests are idempotent (multiple requests have the same effect). Note that the biggest difference between POST and PUT is idempotency, so PUT can also be used for create operations, as long as the id of the resource is determined before the creation.

One of the benefits of putting ids in urls instead of Query Param is that you can indicate hierarchical relationships between resources, such as comments and likes that follow articles. These two resources must belong to the same article, so their urls should look Like this:

Comment on:

  • GET /articles/aid/comments: a list of comments on an article
  • GET /comments/cid: get
  • POST /articles/aid/comments: Creates a comment on an article
  • PUT /comments/cid: Modify comments
  • DELETE /comments/cid: deletes comments

    Here there is a bit more special, always to use the resources of can point to the shortest URL path, that is to say, since/comments/cid already can point to a comment, just don’t need to use/articles/aid/comments/cid specially pointed out that belongs to the article.

    Thumb up:

  • GET /articles/id/like: Check whether the article has been liked

  • PUT /articles/id/like: Like the article
  • DELETE /articles/id/like: Unlike

Verbs are not recommended in RESTful, so this relationship can be mapped as a resource. And because most of the relationship queries are related to the current logged-in user, you can also return the relationship state directly from the resource to which the relationship belongs. Like status, for example, can be returned directly when retrieving article details. Notice that I chose PUT instead of POST, because I think the like behavior should be idempotent, and the result should be the same multiple times.

Token and Sign

The API needs to be designed to be stateless, so the client needs to provide a valid Token and Sign on every request, which in my opinion are used for:

  • The Token is used to prove the user to which the request belongs. Generally, the server randomly generates a string (UUID) after login to bind the user and returns the string to the client. There are two ways to maintain the status of tokens: One is that the TOKEN lifetime is extended or reset with each operation (similar to the caching mechanism), and the other is that the TOKEN lifetime is fixed, but at the same time, a TOKEN for refreshing is returned. When the TOKEN expires, it can be refreshed instead of re-logging in.
  • Sign is used to prove that the request is reasonable. Generally, the client will concatenate the request parameters and encrypt them to the server as Sign. In this way, even if the packet is captured, the server will detect the request if the other party only modifies the parameters but fails to generate the corresponding Sign. You can also add a timestamp, request address, and Token to the Sign, so that the Sign also has an owner, timeliness, and destination.

Statistical parameter

I’m not sure this kind of parameters specific the what is called, anyhow is various user privacy 【. Similar to the latitude and longitude, mobile phone system, model number, IMEI, network status, client version, channels, etc., these parameters will be used to collect and then used as the platform, such as operation, statistics but in most cases they are has nothing to do with the business. If these parameters change infrequently, they can be submitted during login, and if they change frequently, they can be submitted in rotation or in other requests.

Business parameters

In RESTful standards, both PUT and PATCH can be used for modifying operations. The difference between PUT and PATCH is that PUT needs to submit the entire object, while PATCH only needs to submit modified information. But I don’t think it’s necessary in practice, so I’ll just use PUT and submit only the modified information.

Another question is whether it is better to use form submission or JSON submission when creating objects through POST. You can do either, but the only difference in my opinion is that JSON makes it easier to represent more complex structures (with nested objects). And whatever you use, keep it the same and don’t mix the two.

Another recommendation is to leave filtering, paging, and sorting information to the client, including filtering criteria, page count or cursor, number of pages per page, sorting methods, ascending and descending order, etc., to make the API more flexible. However, for filtering conditions, sorting methods, etc., it is not necessary to support all methods, just need to support the method that is used now and may be used in the future, and parsing through string enumeration, so that visibility is better. Such as:

Search, the client provides only the keyword, the specific search field, and the search method (prefix, full text, precise) is determined by the server:

/users/? query=ScienJusCopy the code

Filter, only need to support existing cases:

/users/? gender=1Copy the code

For some specific and complex business logic, instead of trying to get clients to represent it with complex query parameters, use aliases in urls:

/users/recommend
Copy the code

Page:

/users/? offset=10&limit=10 /articles/? cursor=2015-01-01 15:20:30&limit=10 /users/? page=2&pre_page=20Copy the code

Sort, only need to support the existing case:

/articles/sort=-create_date
Copy the code

PS: I really like the way field names are preceded by – to indicate descending order.

The response

Use HTTP status codes as much as possible. Common ones are:

  • 200: Request succeeded
  • 201: Created or modified successfully
  • 204: Deleted successfully
  • 400: The parameter is incorrect
  • 401: Not logged in
  • 403: Access is prohibited
  • 404: Not found
  • 500: system error

    However, there are times when using HTTP status codes alone cannot express an error message explicitly, so I prefer to add a layer of custom return codes, such as:

    Success:

{"code": 100, "MSG ":" success ", "data": {}Copy the code

Failure:

{"code": -1000, "MSG ":" username or password error "}Copy the code

Data is the data that really needs to be returned and only exists when the request succeeds, MSG is only used in the development environment and is identified by the developer only. The client logic only allows code to be identified and does not allow the contents of MSG to be presented directly to the user. If the error is too complex to be described in a single paragraph, you can also add a doc field with a link to the document where the error occurred.

Return the data

JSON visualizes better than XML and saves traffic, so try not to use XML.

After the creation or modification is successful, all information about the resource must be returned.

The returned data should not be strongly coupled to the client interface, and the API should not be designed with the performance benefit of fewer queries to an associated table or fewer queries/returns to several fields. Even if the client needs to display multiple resources on a page, do not return them all in one interface. Instead, let the client request multiple interfaces separately.

It is best to encrypt and compress the returned data, especially when compression is important in mobile applications.

paging

As mentioned in the backend page design of APP, the page layout is generally divided into two types, one is elevator page with bottom part bar, which is common in the Web side, and the other is pull-up load more streaming page, which is common in APP. How do you design these two paging apis?

Elevator paging requires page (number of pages) and PRE_page (number of pages per page). Such as:

/users/? page=2&pre_page=20Copy the code

In addition, the server needs to return total_count (total number of records), as well as the optional number of current pages, number of pages per page (both of which are the same as those submitted by the client), total number of pages, whether there is a next page, and whether there is a previous page (all three can be calculated from total number of records). Such as:

{
    "pagination": {
       "previous": 1,
       "next": 3,
       "current": 2,
       "per_page": 20,
       "total": 200,
       "pages": 10
    },
    "data": {}
}
Copy the code

Streaming layouts can also be used in this way without the need to query the total number of records (the benefit is one less database operation, and the disadvantage is that the client has to make one more request to determine whether it is on the last page). However, data duplication and missing can occur, so cursor paging is recommended.

Cursor paging requires providing cursor(the start cursor of the next page) and limit(quantity) parameters. Such as:

/articles/? cursor=2015-01-01 15:20:30&limit=10Copy the code

If the list of articles is created in reverse order by default, then the cursor is the creation time of the last item in the current list (the first page is the current time).

The data that the server needs to return is very simple, just the total number of records starting from this cursor and the next starting cursor. Such as:

{
    "pagination": {
       "next": "2015-01-01 12:20:30",
       "limit": 10,
       "total": 100,
    },
    "data": {}
}
Copy the code

If total is less than limit, there is no data left.

Another situation that is common with the paging apis of streaming layouts is incremental updates to the drop-down refresh. The business logic is exactly the opposite of cursor paging, but the parameters are basically the same:

/articles/? cursor=2015-01-01 15:20:30&limit=20Copy the code

There are two ways to return the data. One is to return all the incremental data if the number of incremental updates is less than the specified amount (the amount can be set to be relatively large), and the client will add the incremental data to the top of the existing list. However, if the number of incremental updates is larger than the specified number, only the latest N entries are returned as the first page, and the client needs to clear the previous list. Such as:

{
    "pagination": {
       "limit": 20,
       "total": 100,
    },
    "data": {}
}
Copy the code

If total is greater than limit, too much data is being incremented so only the first page is returned and the old list needs to be cleared.