It has come to my attention that, beyond REST, there’s very little agreement on how an API should work. In fact, there’re so many conflicting opinions out there you might as well make up your own convention – so that’s exactly what I’m going to do.

Feel free to use this as you build your own APIs. Or don’t, it’s up to you. I wrote this to alleviate my own decision fatigue when building APIs; maybe it will help someone else reduce theirs?

This is a living document and will get updated as I encounter new problems or decision points in my own work.

Resource versioning

All resources must be versioned

All API resources must be versioned in the following manner: https://api.example.com/v1/books Even simple resources, like ones being used for health checks or version information, must contain a version.

Rationale: It’s impossible to know ahead of time what breaking changes you’ll need to make, and you don’t want to have version information and resources fighting on the same directory level.

Breaking changes necessitate version bumps

A change to the contract of a resource will result in that resource’s version increasing by one: https://api.example.com/v2/books The old version of the resource should remain functional.

Rationale: A change in contract will require users of the API to opt-in slowly over time.

As a suggestion, the service powering the API should translate previous version calls into new version calls to reduce code duplication and to make the API service easier to maintain as versions grow.

Versions must be specific to each resource

A version increase in a single resource should not result in a version increase to other, unaffected resources.

Rationale: A version increase signifies a contract change. If the contract on a separate resource didn’t change, it should remain on the previous version. Complexity of implementation aside, it’s confusing for API consumers to have two versions of the same resource that behave in exactly the same manner.

Multi-word strings

In paths

Word separation must be done by kebab-case in paths.

$ curl -X GET \
https://api.example.com/v1/book/ca103447-630a-4a87-95d0-031e1535231f/page-count

{"pages": 42}

Rationale: Hyphens won. Nearly every major website and CMS uses hyphens as word separators. While this change was originally done to meet SEO guidelines, it’s become so ubiquitous in URIs that there’s no reason not to adopt it as a convention for API paths as well.

In parameters, keys, and path variables

Word separation must be done by snake_case in parameters and JSON document keys.

$ curl -X GET https://api.example.com/v1/authors\?last_name\=Shakespeare

{"items": [{"first_name": "William", "last_name": "Shakespeare"}]}

snake_case must also be used when documenting path variables, e.g. /v1/book/{book_id}/page-count. Path variables must be surrounded by {} to distinguish them as modifiable aspects of the path.

Rationale: Parameters are variables, not paths; JSON document keys are not variables, but are frequently used as parameters when filtering, and as such should be subject to the same conventions as parameters. Few languages consider kebab-case to be valid variable names. This makes keys more cumbersome to access in a number of languages; consider author['first-name'] instead of author.first_name.

This leaves snake_case and camelCase as contenders. Both are valid variable names in most languages. I prefer snake_case as it shares similarities with the kebab-case used in paths. Both snake_case and kebab-case use a single-character word delimiter and are uniformly lowercase.

Resource names

See also the resource naming guide from restfulapi.net. A few things that are worth reiterating:

Plural nouns

A plural resource must always take or return an array of items. This is true even if you only return a single item.

For example, the endpoint POST /v1/books should accept one or multiple books, while POST /v1/book should accept at most one.

Singular resource format

Singular resources should accept and return a single item.

{
    "title": "A Tale of Two Cities",
    "author": "Charles Dickens"
}
Singular resource

Plural resource format

Plural resources should accept and return an object containing an array of items. The array may be empty, contain a single item, or contain multiple items.

The top-level keys of the body should contain any metadata about the request or response. For example, it may include the number of responses, the time the response took, or pagination information.

The body must contain a key named items which corresponds to the array of items being sent or received. The array can be empty, but the key must exist, and its value must never be null.

{
    "count": 1,
    "items": [
        {
            "title": "A Tale of Two Cities",
            "author": "Charles Dickens"
        }
    ]
}
Plural resource

Batch error handling

If a resource supports operations on multiple items at once, and the operation fails on any number of those items, the API must return failure with a list of the items that failed.

Avoid filtering resources

Resources should not be used to filter other resources. For example, if a resource exists named /v1/books, there should not be a resource called /v1/books/award-winners to return a subset of /v1/books.

Given this example, if the return of /v1/books/award-winners is a list of books in an identical format to /v1/books, then this psuedo-resource would be better served as a filter parameter to /v1/books, such as award_winner=true.

If, on the other hand, the return of /v1/books/award-winners looks nothing like the format of /v1/books (for example, containing nothing besides the book’s ISBN and the award it received), it should exist as a different top-level resource, such as /v1/awards.

Resource identifiers

The only variables that should be in paths are identifiers. Do not get cute and come up with multiple ways to access the same information. Each item should have one unambiguous, URL-friendly identifier.

UUIDs suit this purpose very well and enjoy widespread adoption, but they are not required. However, identifiers must be non-sequential.

Rationale: Sequential identifiers imply an order where none is guaranteed to exist. Sequential identifiers also allow end users to enumerate the API. Security aside, it’s unlikely you would want an end user to consume from your API in such an inefficient manner. You should provide the information in a more easily-consumable fashion or not at all. Sequential identifiers creates an opportunity for users to lazily hack around the API.

Filtering

Simple filtering

Filtering capabilities are not needed for all resources.

When a resource is capable of being filtered, it should be filtered via GET parameters. These parameters should follow the same snake_case conventions as discussed in the “multi-word strings” section.

Comparison-based filtering (greater than, less than) should be spelled out in plain language, e.g. published_after=1577836800.

Filters are implicit ANDs, never ORs. It should be expected that users can mix and match filters as they see fit.

If the concept of mixing multiple keys frightens you because you’re using prepared SQL, consider this pattern:

-- Example for PostgreSQL / psycopg2, but easily extrapolated to other systems.
SELECT title, author FROM books
WHERE (%(author)s IS NULL OR author = %s(author)s)
AND (%(title)s IS NULL OR title = %(title)s);

Advanced filtering

If you need more than a few simple parameters to allow users to filter your data, you either lack an understanding of your access pattern or you are unwittingly reinventing Elasticsearch. You should probably either restructure your API or use Elasticsearch instead.

HTTP verb behavior

New item creation

If a resource supports creating new items via POST, but does not support the same via PUT, the POST call can either be idempotent or not idempotent.

An idempotent POST is the equivalent of an UPSERT: if an item whose unique identifier field(s) does not exist, an idempotent POST will create it. If an item matching those unique identifier field(s) does exist, an idempotent POST will update it.

However, if a resource supports PUT, the POST call must not be idempotent, and the PUT call must not be permitted to create new items.

Rationale: It is generally accepted that PUT is an idempotent call. Since this is the case, POST must not be idempotent, otherwise both PUT and POST would be redundant. Additionally, a PUT on a non-existent resource should always fail, otherwise the PUT would be the equivalent of an idempotent POST.

Other resources