Documents how to write APIs
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.
- API Manifesto
- Table of Contents
- Requests
- Responses
- Status Codes
- Auth
- Error Handling
- Localization
- Timeouts
- Pagination
- Inspiration
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
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"
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 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 are used to indicate what action to perform with the resource.
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 is used to create new resources.
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.
With PUT calls we can replace entire objects. Only the database identifier should not be changed.
To delete a resource, use the DELETE method.
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.
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"
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"
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.
See:
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": "..."
}
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.
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": ""
}
}
Click to see examples
It's ok to use all available response codes, See list
Here is a list of the commonly used
200
-> OK, used when onGET
request with successful response201
-> Created, used onPOST
creating a record in DB202
-> Accepted, used when request has been received, but processed async204
-> No Content, used when no response is send, e.g. onDELETE
301
-> Moved Permanently, used if the resource has been moved to anotherURI
304
-> Not Modified, used ifIf-Modified-Since
header is send and nothing has changed since
400
-> Bad Request, used when request cannot be processed, remember to give more info401
-> Unauthorized, used when authorization session is invalid or missing403
-> Forbidden, used when a route / entity was requested, but users access level does not permit it404
-> Not Found, used when a route / entity was not found405
-> Method Not Allowed, used when a route was hit with wrong method409
-> Conflict, used when an entity conflicts with another entity, e.g. duplicate entities / IDs422
-> Unprocessable Entity, used when validation rules onPOST/PATCH/PUT
are not followed429
-> Too Many Requests, used when you want to rate limit your API
500
-> Internal Server Error, used for undefined server errors, should store a record in an bug tracking tool like Bugsnag, Crashlytics, Rollbar, New relic501
-> 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 architecture503
-> Service Unavailable, used when an external service was not reachable, e.g. twilio.com504
-> 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
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:
- 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 should contain following data:
{
"access_token": "<access_token>",
"refresh_token": "<refresh_token>",
"expires_in": <seconds>
}
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.
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"
}
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.
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 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
These guidelines have been made with inspiration from the following API guidelines: