A Taxonomy of HTTP APIs

A Taxonomy of HTTP APIs

Truly, this should be called An Incomplete Taxonomy of HTTP APIs, but I went with the briefer title. It covers the patterns I’ve come across over seven years working as a software engineer. I will no doubt encounter more in the coming years, and might write part two of this post at some point.

In general, I’ve seen two broad patterns of HTTP API:

REST which centres around resources and is well suited to CRUD operations on data (or any entity that can be represented in text). Services typically provide REST APIs to front end clients and technical users.

RPC which centres around procedures and is better suited to do specific things that aren’t covered by CRUD operations. Purely RPC APIs are typically used for inter-service communication.

RESTful Style

This section outlines some core concepts of REST and outlines basic conventions for common use cases.

Concepts

The REST acronym is used very widely, but the concepts it stands for are discussed less often.

Uniform Interface

The notion of Representational State Transfer or REST originates from Roy Thomas Fielding’s 2000 Ph.D. Thesis. For a system to be considered RESTful, Fielding holds that it must satisfy some architectural constraints. Luckily, most of these are already met by the tools and technologies we use in the industry; they include the use of a layered client-server architecture, statelessness, and support for caching. The constrain we don’t always meet is providing a uniform interface.

Fielding states that a uniform interface requires the maintenance of four additional constraints:

  • identification of resources (which is covered by URI Path Naming and API Behaviour below)

  • manipulation of resources through representations (which is outlined below)

  • self-descriptive messages (also covered by URI Path Naming and API Behaviour)

  • hypermedia as the engine of application state (Most REST APIs I’ve seen don’t adhere to HATEOAS — responses for a given resource tend to reference other resources by a local ID rather than by path or URL as HATEOAS mandates)

This document is mainly concerned with the first three constraints. See this article for an account of many standard approaches to HATEOAS.

For a more detailed (and readable) account of the four uniform interface constraints, see chapter 4 of The Little Book on REST Services.

Resources and Representations

The key abstraction of information in REST is a resource. Any information that can be named can be a resource: a document or image, a temporal service (e.g. "today's weather in Los Angeles"), a collection of other resources, a non-virtual object (e.g. a person), and so on. In other words, any concept that might be the target of an author's hypertext reference must fit within the definition of a resource. A resource is a conceptual mapping to a set of entities, not the entity that corresponds to the mapping at any particular point in time. (Fielding)

The examples above may seem a little abstract. In our context, resources are likely to be business entities like users in an application database, pods in a Kubernetes cluster or cloud resources such as AWS EC2 instances and S3 buckets.

It’s worth baring in mind the broadness of the definition, however, and remembering that resources need not correspond to database tables. In the simplest sense, a resource is anything that can be identified and represented. For most of us, this usually means “identified by a URL” and “represented in JSON”, although REST is agnostic to these standards.

The representations through which Fielding proposed we manipulate resources are simply “sequence[s] of bytes, plus representation metadata to describe those bytes”. For us, these representations are usually JSON data in request and response bodies. This data should represent the current or desired state of an identified resource. In this way, through successive requests and responses, representations of resources are used to drive state change. As Fielding puts it “[t]he model application is […] an engine that moves from one state to the next by examining and choosing from among the alternative state transitions in the current set of representations.”

URI Path Naming and API Behaviour

This section is a bit more opinionated, and covers the structure of uniform resource identifier paths and basic server behaviour. It identifies HTTP methods for common operations on resources. It doesn’t deal with response codes, headers, or how data in request and response bodies are structured.

REST APIs expose actions on resources. The paths for these resources end in either nouns or identifiers such as integers and UUIDs. E.g.

  • /api/things

  • /api/things/1

Unlike paths ending in verbs, these paths are resource-oriented.

Creating

Requests for creating things look like this:

POST /api/things HTTP/1.1
Content-Type: application/json 

{"name": "a thing"}

POST is the most common way of creating resources.

In cases where clients' behaviour or network conditions could lead to multiple duplicate resources being created using POST, it may be worth considering using PUT for creation.

PUT /api/things/1fc5861f-7a53-4e9b-ac36-7235f441af3e HTTP/1.1
Content-Type: application/json

{"name": "a thing"}

If PUT doesn’t provide adequate idempotence, it may be with looking into implementing this draft standard: The Idempotency HTTP Header Field.

Reading

GET requests shouldn’t modify state. (See rfc7231 section 4.2.1)

One or more representations can be read from resources.

Reading One Thing

URI paths for getting single resources end with identifiers like integers and UUIDs.

Example request:

GET /api/things/1 HTTP/1.1

Example response:

HTTP/1.1 200 OK
Content-Type: application/json

{"name": "a thing"}

One pattern I’ve seen is responding with a 404 status code whenever a client is forbidden access to an individual resource.

“An origin server that wishes to "hide" the current existence of a forbidden target resource MAY instead respond with a status code of 404 (Not Found).” (RFC 7231: Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content)

Reading Many Things

Some URIs identify collections of resources. These end with plural nouns. Responses include representations of all resources in the collection. If the request is authenticated so that it is associated with a particular user, then the response contains all data that the user has access to.

Example request:

GET /api/things HTTP/1.1
Authorization: bearer UDvvBErOmcYbXJXLnze3dmGkzLD8GG

Example response:

HTTP/1.1 200 OK
Content-Type: application/json

[
    {"name": "a thing", "foo": "bar"},
    {"name": "another thing", "foo": "baz"}
]
  • They will get a list of representations of things.

  • The list will only include things accessible to the user associated with the authorization token.

Client explicitly use the query string to retrieve a subset of the data

Example request:

GET /api/things?foo=baz HTTP/1.1
Authorization: bearer UDvvBErOmcYbXJXLnze3dmGkzLD8GG

Example response:

HTTP/1.1 200 OK
Content-Type: application/json

[
    {"name": "another thing", "foo": "baz"}
]

GET /api/things?foo=bar HTTP/1.1

Implicit filtering?
There are cases where it isn’t possible, or desirable, to filter explicitly. These include: legacy mobile clients existing in the wild that can only handle a subset of the data, so by default, the data would need to be filtered; put the the filtering in the back-end for BFFs, so that it can change independent; implement something like soft deletion and hide the deleted records from the client.

Updating

PATCH is commonly used for updating resources. It allows clients to replace only part of a representation. PUT can also be used but it is less flexible and could easily lead to data being unintentionally overwritten and lost if there were different client versions in the wild.

PATCH /api/things/1 HTTP/1.1
Content-Type: application/json

{"name": "New year -- new thing"}

Deleting

Deleting a single resource RESTfully is simple.

Example request:

DELETE /api/things/1 HTTP/1.1

Example response:

HTTP/1.1 204 No Content

Common pain-point: deleting multiple things

Most people have run into a bulk delete use case. Sadly there isn’t an agreed-upon RESTful way of doing this.

The most robust way is the to put the IDs of the resources to delete in the body However, OpenAPI 3.0 forbids DELETE requests with bodies, while OpenAPI 3.1 merely recommends against DELETE requests with bodies. This is based on the HTTP standards document: “A payload within a DELETE request message has no defined semantics” RFC 7231: Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content

Personally, I would lean toward using an RPC-style over REST for bulk deletes.

Example request:

POST /api/bulk-delete-things HTTP/1.1
Content-Type: application/json

{"ids": [1, 4, 55, 222]}

Example response:

HTTP/1.1 200 OK

RPC Style

There are some cases when a client just needs to tell a server to do something. This thing could be triggering an asynchronous task for example. In this case, it’s simplest to treat an HTTP request as a function call.

While it is possible to execute tasks like this RESTfully, it’s often impractical to do so. One would need to create a resource that tracks the execution of the task and clients would need to POST to this resource when initiating the task, then poll the resource for progress.

Unlike RESTful paths, RPC paths always end with verbs.

Example request:

POST /api/do-something HTTP/1.1
Content-Type: application/json

{"foo": true, "bar": 42}

Example response:

HTTP/1.1 200 OK

RPC endpoints use the POST method and accept any arguments in the request body.

Engineers who are used to REST can be quite disparaging about RPC-style APIs. I’ve heard people sneer about “verbs in the path”. However, this appears to be changing. This article does a good job of covering the advantages and trade-offs of both styles.

The article mentioned above was written by engineer at Google’s and touches on Google’s technologies: gRPC/protobuff. If you find yourself writing a completely RPC-style API with HTTP/JSON it may be worth looking at these alternatives.

Hybrid Style

Sometimes a client needs to perform an action on a resource that is not covered by the HTTP methods. The pragmatic solution is to stick a verb on the end of the path. This may not be everyone’s cup of tea but it’s supported by frameworks and used to solve real problems.

POST /api/things/1/publish HTTP/1.1
Content-Type: application/json

{"at_time": "2024-10-19T14:00Z"}

It is also possible to do the same thing in a RESTful way. In the (admittedly simple and contrived) example given, the same could be achieved with this request:

PATCH /api/things/1/ HTTP/1.1
Content-Type: application/json

{"publication_time": "2024-10-19T14:00Z"}

Conclusion

While I have attempted to keep this dry and objective, I’ve still ended up writing about trade-offs and edge cases. Not to mention opinions! APIs are an area where back-end and front-end engineers tend to argue with each-other and even amongst themselves.

Much like how there is renewed acceptance of functional and imperative programming, and object-oriented programming isn’t as popular as it once was, REST isn’t always considered better than RPC anymore. Even though I’ve written more on REST (due to my experience) I think that both REST and RPC can be valid approaches depending on the problem one is solving.

RPC gets stuff done and tools like gRPC can offer very fast and practical solutions. While hardly anyone implements HATEOAS and we probably haven’t lived up to Fielding’s dream of a uniform REST interface, it is useful that engineers tend to know what to expect from HTTP APIs.

References