Good Practices on RESTful API Modeling (Part 2)
Photograph by Thomas Lee

Good Practices on RESTful API Modeling (Part 2)

By Dr. Thomas Lee, CEO, Throput. This is a re-posting of a Throput blog article.

In my last article, I compared the two approaches of modeling RESTful APIs: resource-oriented vs operation-oriented. In this article, I’ll discuss practices of designing an resource-oriented API request.

An API request essentially consists of four parts: URL endpoint, query string, request body, and HTTP method.

URL Endpoint

An URL endpoint starts with the base path followed by the request path. Optionally, we may give the operation specifier at the very end:

  • Segment 1: base bath
  • Segment 2: request path
  • Segment 3: operation specifier

The base path is typically in form of https://ex.throput.net/api/v2 where v2 is the version number of the API. (Multiple versions of the same API may co-exist.) Alternatively, the version number can be specified in the Accept HTTP header of the request instead of the URL path, e.g., Accept: application/json; version=1.

The request path uniquely identifies the resource, in form of resource1class/resource1id/resource2class/resource2id/resource3class/resource3id, where resource3 is a subclass of resource2, and resource2 is a subclass of resource1. For example, regions/hk/companies/xyzco/staff/1234 specifies a resource of the staff (resource class) of ID 1234 (resource ID) in the company (resource class) of name XYZ Co. (resource ID) in the region (resource class) Hong Kong (resource ID).

In the resource-oriented style of API modeling, the operation of an API request is usually specified as an HTTP method when it can fit into the CRUD (Create-Read-Update-Delete) model. (We’ll come back to this later.) However, in some cases, when the operation is not exactly a CRUD, we can add a suffix to the endpoint to specify the operation, e.g., bookroom to request a meeting room.

Here is an URL endpoint example combining the above three segments:

https://ex.throput.net/api/v2/regions/hk/companies/xyzco/staff/1234/bookroom

Note that the resource class is conventionally named with a plural noun because it represents a collection of resources. For example, the endpoint https://ex.throput.net/regions/hk/companies/ represents the collection of companies in Hong Kong. When we HTTP GET this endpoint, we request to list all companies in Hong Kong. When we HTTP GET https://ex.throput.net/regions/hk/companies?name_contains=xyz, we may find a collection of companies whose names contain ‘xyz’. To refer to a specific company, we should use https://ex.throput.net/api/v2/regions/hk/companies/xyzco where xyzco is regarded as the company ID.

Query String

An optional query string followed by the endpoint can be added to specify the parameters that qualify the API request. For example, when listing the resources in a collection with an HTTP GET operation, filters can be added in the query string, e.g., name_contains=xyz, price_gt=300 (price greater than $300) . In the above example, when we only want to request a room in a specific location as follows:

https://ex.throput.net/api/v2/region/hk/company/xyzco/staff/1234/bookroom?location=kowloon

HTTP Methods

We may be familiar with common HTTP methods, such as  POST, GETPUTDELETE, which are roughly mapped into the CRUD operations as follows:

  • HTTP POST → CREATE
  • HTTP GET → READ
  • HTTP PUT → UPDATE
  • HTTP DELETE → DELETE

The following table summarizes the properties of the above HTTP methods.

I do not plan to detail the definitions of these HTTP methods here. Instead I am going to highlight some important considerations in selecting the methods . Rather than sticking to the above CRUD mapping, I tend to consider the properties of HTTP Methods when selecting one of them to meet the requirements of a specific operation.

For example, HTTP PUT is supposed to be idempotent while HTTP POST isn’t idempotent. When an operation is idempotent, the server state does not change after the operation is executed once or multiple times.

An operation to create a record in the database can be implemented in two ways. The first way is that the operation requires the client to supply a record ID, as well as other fields. If the given ID does not exist in the database, the record is supposed to be new and the request can successfully create it in the database (provided that all other fields are valid). Later, when the client issues another request to create the same record with the identical ID, the request will fail and receive the 409 CONFLICT client error. Only one record is created although the operation is called twice. Since this Create operation is idempotent, I’ll use HTTP PUT. (In reality, HTTP POST is fine too while many people do this.)

Alternatively, if the record ID is generated by the server rather than given by the client, when the client issues two requests even with all same field values, two separate records will be created, with two different server-generated IDs. In that case, the operation is not idempotent and I’ll use HTTP POST to implement it. Besides idempotence, I’ll also consider whether the operation is safe (i.e., changes the server state), contains data in request and response bodies, and supports HTML forms.

Instead of blindly following the CRUD mapping, We should refer to the definitions of HTTP methods when we select them to implement our operations. Some middlewares (e.g., cache servers) and content delivery networks (CDNs) follow these definitions and implement different behaviors for different HTTP methods.

Apart from the above HTTP methods, there are others which can also be applied in common scenarios, such as HTTP HEAD, HTTP PATCH. I’ll let you study their definitions and find out how they can be applied.

HTTP Request Body

While HTTP request body is commonly serialized as text formats, e.g., JSON, XML, YAML, binary serialization formats such as ThriftAvroProtocol BuffersBSON, and MessagePack can also be used. The binary formats are more efficient but less verbose (thus less convenient for debugging). We can use Content-Type to specify the media type of the request body while the Accept header specifies the excepted media type of the response body. (The HTTP request and the corresponding HTTP response commonly share the same media type.) Most of the common media types have been standardized by the Internet Assigned Numbers Authority (IANA).

If we plan to use JSON as the request body format, we may consider to adopt the JSON:API specification, which provides a reference body structure for API requests and responses. When we choose to use JSON:API, we need not struggle to define our own JSON structure but we should expect some extra complexity introduced by the specification.

Conclusion

Although there seems to be a lot of freedom in designing RESTful API requests, implementing them without understanding the fundamental principles of HTTP and REST would lead to inconsistency of design, difficulties in usage, and complications during maintenance. In addition, many HTTP platforms (e.g., middlewares) have been implemented based on these principles, not following these principles could also cause system performance or data integrity issues when we adopt such platforms to deliver our APIs.

回复
Jason Polis

Financial Data Communication + API Standards

6 年

Good article. Json:api could be of value.

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

Thomas Lee的更多文章

社区洞察

其他会员也浏览了