Skip to content

ml-opensource/API-manifesto

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

66 Commits
 
 
 
 

Repository files navigation

API Manifesto

Documents how to write APIs

Introduction

This API Manifesto is a fast and easy overview of the most important elements for building a rock-solid API, which both the backend and frontend team enjoys working with. APIs are suppose to be very strict, it's a contract between Backend & Frontend. In the same time, there is no reason for APIs to be different depending on which language / framework was used in the Backend.

This is not a full blown manifesto, check the Inspiration section for references to other manifestos with more details.

Table of Contents

Requests

URLs

Prefix API endpoints

Prefix API endpoints with /api/ to separate them from other URLs like HTML views served on the same server.

Click to see examples

Make sure to prefix your API endpoints with /api/:

www.example.com/api/v1/products/1

API versioning

Versioning your API allows you to make non-backwards compatible changes to your API for newer clients by introducing new versions of endpoints while not breaking existing clients.

Include the API version in the URL. Versions start with 1 and are prefixed with v. The version path component should come right after the api path component.

In case of an existing API that doesn't have this versioning scheme but needs a new version, skip v1 and go straight to v2.

We're not recommending to use the headers (typically the Accept header) for versioning. The URL based approach is more obvious, usually simpler to implement, and testing URLs can be done in the browser.

Click to see examples

Include the API's version in the URL:

www.example.com/api/v2/auth/login

⛔️

Don't depend on a header like "Accept" for versioning:

Accept = "application/vnd.example.v1+json"

REST resources

After the API prefix and the version comes the part of the URL path that identifies the resource -- the piece of data you are interested in. Refer to a type of resource with a plural noun (eg. "users"). Directly following such a noun can be an identifier that points to a single instance. A resource can also be nested, usually if there some sort of parent/child relationship. This can be expressed by appending another plural noun to the URL.

Click to see examples

Refer to a resource with a plural noun:

/api/v1/shops

Use an identifier following a noun to refer to a single entity:

/api/v1/products/42

Refer to a nested resource like so:

/api/v1/posts/1/comments

In some cases it can be ok to simplify and have the child object at the beginning as long as the child object's id is globally unique:

/api/v1/comments/87

Be careful with this since this approach lack the extra safety of asserting that the resource you are referring to belongs to the parent resource you think it does.

Query parameters

Query parameters are like meta data to the (usually GET) URL request. They can be used when you need more control over what data should be returned. Good use cases include filters and sorting. Some things are better suited for headers, such as providing authentication and indicating the preferred encoding type.

Click to see examples

Use query parameters for a paginated endpoint to define which page and with how many results per page you want to retrieve:

/api/v1/posts?page=2&perPage=10

⛔️

Do not use query parameters for authentication:

/api/v1/posts?apiKey=a7dhas8u

HTTP Methods

HTTP methods are used to indicate what action to perform with the resource.

GET

A GET call is used to retrieve data and should not result in changes to the accessed resource. Multiple identical requests should have the same effect as a single request (idempotency).

POST

POST is used to create new resources.

PATCH

PATCH requests modify existing resources. Only fields that need to be updated need to be included - all others will be left as they are. In order to "unset" optional properties use null for the value.

PUT

With PUT calls we can replace entire objects. Only the database identifier should not be changed.

DELETE

To delete a resource, use the DELETE method.

HEAD

A HEAD call must never return a body. It can be used to see if an object exists and to see if a cached value is still up to date.

Request Headers

Protected endpoints

Use the Authorization header to consume protected endpoints. See the Auth section for more information on how to handle authorization and authentication.

Click to see examples

Use Authorization to authorize:

Authorization = "Basic QWxhZGRpbjpPcGVuU2VzYW1l"

⛔️

Avoid using custom headers for authorization:

UserToken = "QWxhZGRpbjpPcGVuU2VzYW1l"

Supporting localization

In order to support localization now and in the future, the Accept-Language should be used to indicate the client's language towards the API.

Click to see examples

Use ISO 639-1 codes to indicate the preferred language of the response.

Accept-Language = "da"

Use a prioritized list of languages to influence the fallback language:

Accept-Language = "da, en"

⛔️

Avoid using other standards than ISO 639-1 for specifying the preferred language:

Accept-Language = "danish"

Making debugging easier

Use headers to give the API information about the consumer to ease debugging. There's no industry standard, so feel free to make your own convention, just remember to use it consistently.

Click to see examples

Client-Meta-Information = iOS;staging;v1.2;iOS12;iPhone13

See:

Responses

Response Body

Object at the root level

A body should always return an object at the root level. This enables including additional data about the response such as metadata separate from the object(s). We recommend using data for successful requests with meaningful response data and error for unsuccessful requests with error data being returned.

Click to see examples

Returning a collection should be encapsulated in a key:

{
    "data": [
        {
            "username": "..."
        },
        {
            "username": "..."
        }
    ]
}

Returning an object (e.g. a user) should also use the data key:

{
    "data": {
        "username": "..."
    }
}

Returning an error should use the error key:

{
    "error": {
        "description": "..."
    }
}

Please see the error section for more information.

⛔️

Avoid returning collections at the top level in the response:

[
    {
        "email": "..."
    },
    {
        "email": "..."
    }
]

Avoid returning data that are not encapsulated in a root key (data or error):

{
    "error": true,
    "description": "..."
}

Return an empty collection when there are no results

To make it easier for the API consumer, return HTTP status code 200 with an empty collection instead of e.g. 204 with no body.

Click to see examples

Combine HTTP status code 200 with empty collections:

{
    "data": []
}

⛔️

Avoid using HTTP status code 204 for empty collections.

Use null or unset keys that are not set

In case of missing values return them as null or don't include them. Do not use empty objects or empty strings.

Click to see examples

Return a value as null:

{
    "data": {
        "email": null,
        "name": "..."
    }
}

Unset a key without a value:

{
    "data": {
        "name": "..."
    }
}

⛔️

Avoid including a key without a meaningful value:

{
    "data": {
        "name": ""
    }
}

Status Codes

Click to see examples

It's ok to use all available response codes, See list

Here is a list of the commonly used

2xx

  • 200 -> OK, used when on GET request with successful response
  • 201 -> Created, used on POST creating a record in DB
  • 202 -> Accepted, used when request has been received, but processed async
  • 204 -> No Content, used when no response is send, e.g. on DELETE

3xx

  • 301-> Moved Permanently, used if the resource has been moved to another URI
  • 304 -> Not Modified, used if If-Modified-Since header is send and nothing has changed since

4xx

  • 400 -> Bad Request, used when request cannot be processed, remember to give more info
  • 401 -> Unauthorized, used when authorization session is invalid or missing
  • 403 -> Forbidden, used when a route / entity was requested, but users access level does not permit it
  • 404 -> Not Found, used when a route / entity was not found
  • 405 -> Method Not Allowed, used when a route was hit with wrong method
  • 409 -> Conflict, used when an entity conflicts with another entity, e.g. duplicate entities / IDs
  • 422 -> Unprocessable Entity, used when validation rules on POST/PATCH/PUT are not followed
  • 429 -> Too Many Requests, used when you want to rate limit your API

5xx

  • 500 -> Internal Server Error, used for undefined server errors, should store a record in an bug tracking tool like Bugsnag, Crashlytics, Rollbar, New relic
  • 501 -> Not Implemented, used when you want to indicate that the feature/functionality is not implemented (yet)
  • 502 -> Bad Gateway, used when an internal service was not reachable, e.g. in micro service architecture
  • 503 -> Service Unavailable, used when an external service was not reachable, e.g. twilio.com
  • 504 -> Gateway Timeout, used to indicate that a request timed out (e.g. third party service took too long)

⛔️

Custom response codes, eg:

  • 490
  • 205
  • 512

Use response body for the message instead

Auth

Authentication is one of the most essential and important parts of the API. Authentication implementations is highly dependent on the requirements and features of each specific project, so we will not cover all all possible options of implementation. However we will specify a bunch of common requirements that apply to any authentication method:

  • Always use TLS-encrypted connection, when trying to authenticate an user.
  • Always store passwords/secrets hashed/encrypted. Never store passwords/secrets as a plain text. Never implement your own encryption algorithm, use time-tested solutions available for your stack.
  • Never pass sensitive information as query string parameters. It can be logged by a web server, proxy or load balancer and make a risk of data leak.
  • You should return an user’s API token only in these cases:
    • user is successfully created
    • user is successfully authenticated
    • tokens are successfully refreshed

When authentication is implemented by us, we highly encourage following these recommendations:

User authentication

  • Use Authorization HTTP header.
  • Use Bearer scheme (described in RFC6750).
  • Use JWT (described in RFC7591) as a Bearer token.
  • Avoid implementing authorization flow by yourself, use well-known libraries and frameworks instead.

Token payload

Token payload should contain following data:

{
    "access_token": "<access_token>",
    "refresh_token": "<refresh_token>",
    "expires_in": <seconds>
}

3rd party authentication and SSO

For implementing authentication with 3rd party services (e.g. Facebook, Github etc.) or SSO we recommend to use OAuth2.0 or/and OIDC. Client may demand using their IdP such as KeyCloak or Azure Active Directory, but as soon as all these providers implement standard protocols (OAuth2.0, OIDC), the choice of a specific provider does not make any significant changes in implementation of API.

TODO

Click to see examples

⛔️

Error Handling

Click to see examples

The error object needs to have the following:

  • Be consistent
  • Have all required info
  • Easily parsable
  • Should be possible to build a solid UI on top, guiding the user what happened, and how to move on
{
    "error": {
        "localizedTitle": "Title goes here", // Optional title localized for end user
        "localizedMessage": "Message goes here", // Optional message localized for end user
        "message": "Invalid format, digits required", // Message for developer
        "isRecoverable": true, // Is the error handled in the UI is fatal or can it be recovered, eg: try again
        "identifier": "PASSWORD_NOT_FOLLOWING_PATTERN", // Identifier which the consumer of the API can parse and switch case on
        "source": "LoginService" // In micro services architecture, you might want to understand what service
    },
    "payload": {
        "validationErrors": [{
            "field": "password",
            "errors": [{
                    "type": "required",
                    "localizedMessage": "Please enter a password"
                },
                {
                    "type": "regex",
                    "localizedMessage": "Password format should have following: 8 characters, 1 small letter, 1 big letter & 1 number"
                },
            ]
        }]
    },
    "metadata": {
        "errorID": "1234-ABC" // Optional ID for if the error is stored in DB, APM, Bug tracking tools like Bugsnag, Sentry, Rollbar, New Relic etc.
    }
}

⛔️

{
  "error": "Internal server error"
}

Localization

TODO

Click to see examples

⛔️

Timeouts

Generally APIs should respond in less than 250ms on a wired connection. There needs to be a special reason for exceeding that. Further, it is important to understand that it will be harder and more expensive to scale the backend if response times are high.

It's common that the webserver configuration will timeout the request after 30 or 60 seconds.

Client to server

Since you never know what network the client is on, if they are in a metro or 100mBit wifi. Response time can vary a lot for several reason. Therefore set timeouts to:

Click to see examples

Default: 15 sec

File upload APIs: Align with web server timeout (eg 30 or 60 sec)

If you are going to upload files above 5mb, consider having client upload directly to AWS S3, Dropbox etc. And sending path to server.

⛔️

+ 30sec

If API requests are taking more than 2 sec on a wired connection, consider changing the API design. Eg: Put the operation in a queue system like SQS, Redis, Beanstalkd and inform the client about operation is complete by push notification, web socket, email etc.

Server to server

Server to service APIs should always be very stable due to connection being wired and stable. Therefore we can be much more aggressive about timeouts.

Click to see examples

Depending on service: 1-5 sec timeouts.

Implementing a retry system is strongly advised. If the server is not responding in 1-5 sec, there is a high chance they never respond. Just fail and retry, up to a max of 3-5 retries, and then throw an exception.

⛔️

+10 sec

Pagination

TODO

Click to see examples

⛔️

Inspiration

These guidelines have been made with inspiration from the following API guidelines:

About

Documents how to write APIs

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published