part of this DevOps Project Template
Use this standard to review your REST-API's. It's highly evolved and in use with multiple large Dutch companies.
Contents:
-
Build the API with consumers (developers) in mind--as a product in its own right.
- Not for a specific front-end.
- Use use-cases and scenarios to validate your APIs UX.
-
Use a domain model (example domain model), even if it needs to be reverse engineered (keep it simple)
- Base resources and URLs on the entities and relationships of your domain model.
-
Create an OpenAPI file for your API before you start implementing the REST-server
-
Use a naming convention
- Use plural forms for resources (
ordersinstead oforder), it's the datamodelling standard. - Use lowercase in constant parts of paths, e.g:
/lowercase, not/CamelCaseor/UPPERCASE. - Use camelCase field names, e.g.:
fieldName, notFieldNameorfield_name - Use UpperCamelCase object names, e.g.:
TicketObject, notticketObjectorticket_object - Avoid snake_case and kebab-case.
- Use plural forms for resources (
-
Use the HTTP verbs to mean this:
- POST - create and all other non-idempotent operations.
- PUT - replace.
- PATCH - (partial) update.
- GET - read a resource or collection.
- DELETE - remove a resource or collection.
-
Ensure that your GET, PUT, PATCH and DELETE operations are all idempotent.
-
Use HTTP status codes to meaning this:
- 102 - Processing. Returned as long as a asynchronous response is still pending. See also 202 Accepted.
- 200 - Success.
- 201 - Created. Returned on successful creation of a new resource. Include a 'Location' header with a link to the newly-created resource.
- 202 - Accepted. Returned to indicated an asynchronous response will be given. Include a 'Location' HTTP-header with URL of the future resource. See also 102 Processing.
- 204 - No content (for empty responses)
- 304 - Not modified. Returned when a client asks for an etag it already received (sent in the request headers). See caching below.
- 400 - Bad request. Data issues such as invalid JSON, etc.
- 404 - Not found. Resource not found on GET.
- 409 - Conflict. Duplicate data or invalid data state would occur.
-
Use the Collection Metaphor, it's intuitive.
- Two URLs per public resource in the domain model:
-
The resource collection (e.g.
/orders) -
Individual resource within the collection (e.g.
/orders/{keytype}/{key}).e.g. POST /orders GET /orders GET /orders/orderid/$id PUT /orders/orderid/$id PATCH /orders/orderid/$id DELETE /orders/orderid/$id
-
- Two URLs per public resource in the domain model:
-
Reflect the hierarchy of the domain model in the URLs, use the same names
-
The first path-segment is the type of the resource in the response
e.g. GET /wholes/$id // returns a 'whole' GET /parts/wholeid/$id // returns 'parts' for a specified whole GET /parts/partsid/$id // returns a part by it id -
Don't be dogmatic, flat and non-domain-model URLs are sometimes needed.
e.g. GET /parts/[subpartid}] GET /frontpage
-
-
-
A normal version number MUST take the form X.Y where X is the major version and Y is the minor version.
-
Minor version MUST be incremented for any release which maintains backwards compatibility to the public API.
-
Major version MUST be incremented if any backwards incompatible changes are introduced to the public API.
-
Keep the API backward compatible as long as possible / avoid breaking changes
-
API's based on a domain model are the most stable
-
Versioning via the URL signifies a 'platform' version and the entire platform must be versioned at the same time
-
Use only major versions in the url as newer minor versions are (should be) backward compatible
e.g. https://api.example.com/v1/orders -
Versioning via the Accept header is versioning the resource, avoid this.
e.g. GET /jobs HTTP/1.1 Host: api.example.com Accept: application/vnd.example.api+json;version=2 -
Additions to a response do not require versioning. However, additions to a request body that are 'required' are troublesome--and may require versioning (breaking changes).
-
-
Responses are part of the interface, don't expose implementation details in them.
- Return domain entities not database entities (use a (logical) domain model not a (technical) data model).
- Don’t expose internal / coupling tables as two IDs.
-
Support sorting and pagination on collections (
?offset=100&limit=50&order=id). -
For large responses, allow clients to select the fields that come back in the response (with query-arguments,
?fields=name&fields=address&fields=city) -
Use UTF-8 character encoding.
-
Use ISO 8601 for dates.
E.g. 1997-07-16T19:20:30+01:00 # time-offset 1997-07-16T19:20:30Z # Zulu-time 1997-07-16T19:20:30EST # time-zone 1997-07-16T19:20:30 # local datetime -
Use ISO 4217 for currency codes.
-
Use ISO 3166 for country codes.
-
Use RFC7807 for error messages.
-
Avoid HATEOAS, while elegant hypermedia linking (HATEOAS) and versioning is troublesome no matter what--minimize it.
-
Don’t use OData, in particular avoid OData function calls as they violate REST-principles (remodel functions calls to resource manipulations)--stick to basic REST.
-
Use OAuth2 to secure your API.
- Use an auto-expiring Bearer token for authentication (
Authorisation: Bearer f0ca4227-64c4-44e1-89e6-b27c62ac2eb6). - Require HTTPS.
- Consider using JSON Web Tokens.
- Use an auto-expiring Bearer token for authentication (
-
Enforce use of the Content-Type and Accept-Type headers even if you use JSON as default for both requests and responses.
e.g. Content-Type: application/json Accept-Type: application/json -
Responses contain header:
X-Content-Type-Options: nosniff -
Responses contain header:
X-Frame-Options: deny -
For multi-lingual APIs, use the Accept-Language header for locale setting (
Accept-Language: nl, en-gb;q=0.8, en;q=0.7) -
All endpoints return the Date header
- Date - Date and time the response was returned (in RFC1123 format) (
Date: Sun, 06 Nov 1994 08:49:37 GMT)
- Date - Date and time the response was returned (in RFC1123 format) (
-
Implement strong caching (by client, transport, proxy, etc.) through the
cache-controlresponse-header. As a minimum have public GET-endpoints return the following response headers:- Cache-Control - The maximum number of seconds (ttl) a response can be cached. (
Cache-Control: public, 360orCache-Control: no-store) - Strong caching minimizes the number of requests a server receives
- Cache-Control - The maximum number of seconds (ttl) a response can be cached. (
-
Consider weak caching through the
ETagresponse-header- ETag - Use a SHA1 hash for the version of a resource. Make sure to include the media type in the hash value, because that makes a different representation. (
ETag: "2dbc2fd2358e1ea1b7a6bc08ea647b9a337ac92d"). The client needs to send a If-None-Match header for this mechanism to work. - Weak caching minimizes the work a server needs to do (but not the number of requests it receives)
- ETag - Use a SHA1 hash for the version of a resource. Make sure to include the media type in the hash value, because that makes a different representation. (
-
Enable header-based caching on all proxies and clients (e.g. NGINX, Apache, APIM) to increase speed and robustness
-
No privacy or security compromising data in URL's
-
Implement and monitor an uncached
/health endpoint, e.g.GET /api/health { "healthy": true, "dependencies": [ { "name": "serviceA", "healthy": true }, { "name": "serviceB", "healthy": true } ] }
For API's that need content / payload encrypytion
-
Implement content encryption on the furthest endpoints (in the REST-server, not the proxies or APIM)
-
When content signing is used, this is done after the content is (optionally) encrypted.
-
Use the X-Signing-Algorithm header to communicate the type of content signing (
X-Signing-Algorithm: RS256) -
Use the X-SHA256-Checksum header to communicate the SHA256 hash value of the content (
X-SHA256-Checksum: e1d58ba0a1810d6dca5f086e6e36a9d81a8d4bb00378bdab30bdb205e7473f87) -
Use the X-Encryption-Algorithm header to communicate the type of content encryption (
X-Encryption-Algorithm: A128CBC-HS256)
A good API (as any other interface) is
- Consistent (avoid surprises by being predictable)
- Cohesive (only lists endpoints with functional dependency)
- Complete (has all necessary endpoints for its purpose)
- Minimal (no more endpoints than neccessary to be complete, no featurism)
- Encapsulating (hiding implementation details)
- Self explaining
- Documented (if self explanation is not sufficient)
On the implementation level, every response is transformed into a request in the following steps:
- check authentication
- check URL
- check authorisation
- validate, decrypt input
- do the actual transformation / CRUD
- encode, encrypt output
- (optionally) send notifications (broadcast the event)
- Don’t use complex design patterns, a REST-server is ‘just’ a pipeline.
- Code for observability (logging, tracing, metrics)
- Code for testability (feature toggles, user rings, failure injection, etc.)
- Monitor Request Latency, Error-rate, Traffic and Cumulative Latency for individual endpoints
| Valid location | Authenticated | Authorized | Valid arguments | status code | |
|---|---|---|---|---|---|
| Check 1 | - | 404 Not found | |||
| Check 2 | + | - | 401 Unauthorized | ||
| Check 3 | + | + | - | 403 Forbidden | |
| Check 4 | + | + | + | - | 400 Bad request |
In asynchronous communication the client does not wait for the answer but either gets called back once the answer is available or checks later.
Client does GET on async endpoint
- Client must supply the webhook in the
X-Callback-Urlheader- Client must supply a UUID-V4 correlation id in the
X-Request-IDheader- Server responds with a status code
202 Acceptedindicating processing has started
REPEAT
Server does POST with result on client webhook
- Server must echo the
X-Request-IDheader in theX-Response-IDheader- Server must use HMAC-SHA256 signing for request authentication (use header
X-Signing-Algorithm: RS256)UNTIL (webhook returns a status code
200 OkOR timed out)
Client does GET on async endpoint
- Server responds with status
202 Acceptedindicating processing has started- Server returns the URL of the future resource in the
Locationheader
DO
Client does GET on the returned URL
WHILE (server responds with status
102 ProcessingAND NOT timed out)