HTTP API Design Guidelines

Foreword

This guide describes a design pattern for HTTP + JSON API

Focus on business logic while avoiding over-design, find a good, consistent, and obvious approach to API design, not the so-called “final/ideal pattern”.

We assume that you are familiar with basic HTTP + JSON API design methods, so this guide does not cover all API design basics.

Directory

  • Base
    • Force the use of secure connections (Secure Connections)
    • Mandatory header information Accept to provide a version number
    • Support Etag cache
    • Provide Request-Id for introspection
    • Split large responses by Range in the request
  • Requests
    • Use JSON format data in the request body
    • Use a unified resource path format
    • Paths and properties should be lowercase
    • Support convenient no-id indirect reference
    • Minimize path nesting
  • Responses
    • return the appropriate status code
    • Provide all available resources
    • Provides the resource’s (UU)ID
    • Provides a standard timestamp
    • Use UTC (Coordinated Universal Time) time, formatted with ISO8601
    • nested foreign key relationship
    • generate structured errors
    • Show frequency limit status
    • Keep response JSON minimal
  • Artifacts
    • Provides a machine-readable JSON schema
    • Provide human-readable documentation
    • Provide executable examples
    • describe stability
  • Translator’s note

Basic

Isolate concerns

Design time keeps things simple by isolating the different parts between the request and the response. Keeping the rules simple allows us to focus on bigger and more difficult problems.

Requests and responses will address a specific resource or collection. Use path (path) to indicate identity, body to transfer content (content) and header information (header) to transfer metadata (metadata). Query parameters can also be used to pass the content of header information, but header information is preferred because they are more flexible and can convey different information.

Force the use of secure connections (Secure Connections)

All API access behaviors need to be accessed through a secure connection using TLS. There is no need to figure out or explain what situations require TLS and what situations do not need TLS, just force all access to pass TLS.

Ideally, avoid any insecure data exchange by denying all non-TLS requests and not responding to http or port 80 requests. If this is not possible in reality, a 403 Forbidden response can be returned.

It is ill-advised to redirect non-TLS requests to TLS connections, as this ambiguous/bad client behavior does not provide significant benefit. Relying on redirected client access not only doubles the server load, but also renders TLS encryption useless because sensitive information is already exposed on the first non-TLS call.

Mandatory header information Accept to provide a version number

Making versions and transitioning smoothly between versions is a huge challenge in designing and maintaining an API. Therefore, it is best to use some methods at the beginning of the design to prevent problems that may be encountered.

In order to avoid unexpected results or call failures caused by changes in the API, it is best to force all access to require a specified version number. Please avoid providing a default version number, once provided, it will be quite difficult to change it later.

The most suitable place to put the version number is the header information (HTTP Headers), using a custom type (content type) in the Accept section to submit along with other metadata (metadata). For example:

Accept: application/vnd. heroku + json; version=3

Support Etag cache

Include ETag header information in all returned responses to identify the version of the resource. This makes it possible for users to cache resources. In subsequent access requests, set the If-None-Match header information to the previously obtained ETag value to detect Detects whether a cached resource needs to be updated.

Provide Request-Id for introspection

Include a Request-Id header with every request response and use the UUID as the value. It gives us a mechanism to trace, diagnose and debug requests by logging this value on the client, server or whatever supporting service.

Split large responses by Range in the request

A large response should be split across multiple requests using the Range header and specify how to get it. Detailed request and response header information (header), status code (status code), range (limit), sorting (ordering) and iteration (iteration), etc.

Requests

Use JSON format data in the request body

Use JSON format data in the body (request bodies) of PUT/PATCH/POST requests instead of using form form data. This corresponds to the request we return in JSON format (request Taobao API interface), for example:

$ curl -X POST https://service.com/apps \
    -H "Content-Type: application/json" \
    -d '{"name": "demoapp"}'

{
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "name": "demoapp",
  "owner": {
    "email": "[email protected]",
    "id": "01234567-89ab-cdef-0123-456789abcdef"
  },
  ...
}

Use a unified resource path format

Resource names

Use the plural form for resource names, unless the resource is a singleton in the system (for example, on most systems, there is only one for a given user account). This approach maintains the uniformity of specific resources.

Actions

Good endings don’t require special behaviors to be specified for resources, but in special cases it is necessary to specify behaviors for some resources. For clarity, add a standard actions before the behavior:

/resources/:resource/actions/:action

For example:

/runs/{run_id}/actions/stop

Paths and properties should be lowercase

In order to be consistent with the naming rules of the domain name, use lowercase letters and separate the path name with -, for example:

service-api.com/users
service-api.com/app-setups

Attributes also use lowercase letters, but attribute names are separated by underscores _ to omit quotation marks in Javascript. For example:

service_class: "first"

Support convenient no-id indirect reference

In some cases, it is inconvenient to ask the user to provide an ID to locate a resource. For example, a user wants to get his app information on the Heroku platform, but the unique identifier of this app is UUID. In this case, you should support the interface to be accessible by both name and ID, for example:

$ curl https://service.com/apps/{app_id_or_name}
$ curl https://service.com/apps/97addcf0-c182
$ curl https://service.com/apps/www-prod

Don’t just accept the use of names and give up the use of ids.

Minimize path nesting

In some resource data modules with parent path/child path nesting relationship, paths may have very deep nesting relationship, for example:

/orgs/{org_id}/apps/{app_id}/dynos/{dyno_id}

It is recommended to specify resources under the root path to limit the nesting depth of paths. Use nested scoped resources. In the above example, dyno belongs to app, app belongs to org can be expressed as:

/orgs/{org_id}
/orgs/{org_id}/apps
/apps/{app_id}
/apps/{app_id}/dynos
/dynos/{dyno_id}

Responses

return the appropriate status code

Return the appropriate HTTP status code for each response. Good responses should use the following status codes:

  • 200: GET request is successful, and DELETE or PATCH synchronous request is completed, or PUT > Synchronously update an existing resource
  • 201: POST synchronous request completed, or PUT synchronously creates a new resource
  • 202: POST, PUT, DELETE, or PATCH request received, will be asynchronous processing
  • 206: GET The request is successful, but only part of it is returned,

Pay attention when using identity card (authentication) and authorization (authorization) error codes:

  • 401 Unauthorized: The user is not authenticated and the request fails
  • 403 Forbidden: The user does not have permission to access the resource, the request failed

When the user requests an error, providing an appropriate status code can provide additional information:

  • 422 Unprocessable Entity: The request was parsed correctly by the server, but contained invalid fields
  • 429 Too Many Requests: Because of frequent access, you have been restricted access, try again later
  • 500 Internal Server Error: server error, confirm status and report problem

For user error and server error condition status codes,

Provide all available resources

Provide all visible resource representations (for example: all properties of this object), and return all available resources when the response code is 200 or 201, including PUT/PATCH and DELETE request, for example:

$ curl -X DELETE \
  https://service.com/apps/1f9b/domains/0fd4

HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
...
{
  "created_at": "2012-01-01T12:00:00Z",
  "hostname": "subdomain.example.com",
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "updated_at": "2012-01-01T12:00:00Z"
}

When the request status code is 202, do not return all available resources, for example:

$ curl -X DELETE \
  https://service.com/apps/1f9b/dynos/05bd

HTTP/1.1 202 Accepted
Content-Type: application/json;charset=utf-8
...
{}

Provides the resource’s (UU)ID

Each resource is given an id attribute by default. Unless you have a better reason, use UUIDs. Do not use identifiers that are not globally unique on the server or resource, especially auto-incrementing ids.

Generate lowercase UUID format 8-4-4-4-12, for example:

"id": "01234567-89ab-cdef-0123-456789abcdef"

Provides a standard timestamp

Provide default creation time created_at and update time updated_at for resources, for example:

{
  ...
  "created_at": "2012-01-01T12:00:00Z",
  "updated_at": "2012-01-01T13:00:00Z",
  ...
}

Some resources do not need to use timestamps, so ignore these two fields.

Use UTC (Coordinated Universal Time) time, formatted with ISO8601

Only accepts and returns times in UTC. Data in ISO8601 format, for example:

"finished_at": "2012-01-01T12:00:00Z"

nested foreign key relationship

Serialize foreign key associations using nested objects, for example:

{
  "name": "service-production",
  "owner": {
    "id": "5d8201b0..."
  },
  //...
}

instead of something like this:

{
  "name": "service-production",
  "owner_id": "5d8201b0...",
  ...
}

This method inlines related resource information as much as possible without changing the resource structure or introducing more top-level fields, for example:

{
  "name": "service-production",
  "owner": {
    "id": "5d8201b0...",
    "name": "Alice",
    "email": "[email protected]"
  },
  ...
}

generate structured errors

When responding with errors, generate uniform, structured error messages. Contains a machine-readable error id, a human-readable error message (message), and a url can be added to tell the client according to the situation For more information about this error and how to fix it, for example:

HTTP/1.1 429 Too Many Requests
{
  "id": "rate_limit",
  "message": "Account reached its API rate limit.",
  "url": "https://docs.service.com/rate-limits"
}

Document the error message format, and the error message id that the client may encounter.

Show frequency limit status

The client’s access speed limit can maintain the good state of the server and ensure high-quality services for other client requests. You can quantify request limits using techniques.

For each request with a RateLimit-Remaining response header, the reserved request tokens are returned.

Keep response JSON minimal

Extra spaces in the request will increase the response size, and many HTTP clients now output JSON in a human-readable format (“prettify”) themselves. So it’s best to keep the response JSON minimal, for example:

{"beta":false,"email":"[email protected]","id":"01234567-89ab-cdef-0123-456789abcdef"," last_login":"2012-01-01T12:00:00Z","created_at":"2012-01-01T12:00:00Z","updated_at":"2012-01- 01T12:00:00Z"}

instead of this:

{
  "beta": false,
  "email": "[email protected]",
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "last_login": "2012-01-01T12:00:00Z",
  "created_at": "2012-01-01T12:00:00Z",
  "updated_at": "2012-01-01T12:00:00Z"
}

You can provide an optional way to provide a more verbose and readable response to the client, using query parameters (eg: ?pretty=true) or via the Accept header parameter ( For example: Accept: application/vnd.heroku + json; version=3; indent=4;).

Artifacts

Provides a machine-readable JSON schema

Provide a machine-readable schema to properly represent your API. Use prmd to manage your schema, and make sure it is valid with prmd verify.

Provide human-readable documentation

Provide human-readable documentation so client developers can understand your API.

If you create a profile with prmd and describe it as described above, you can easily generate Markdown documentation for all nodes using prmd doc.

In addition to node information, provide an API overview information:

  • Verify authorization, including how to obtain and use token.
  • API stability and version management, including how to select the required version.
  • General request and response header information.
  • Wrong serialization format.
  • Examples of API usage by clients in different programming languages.

Provide executable examples

Provide executable examples so that users can directly see the API calls in the terminal, and make these examples easy to use to the greatest extent, so as to reduce the workload of users trying to use the API. For example:

$ export TOKEN=... # acquire from dashboard
$ curl -is https://[email protected]/users

If you use prmd to generate Markdown documents, each node will automatically get some examples.

describe stability

Describe the stability of your API or its completeness and stability in various node environments, for example: add tags such as prototype/development/production.

For more on possible stability and the way changes are managed,

Once your API announces the official version and stable version of the product, don’t make some incompatible changes in the current API version. If you need to, please create a new version of the API.