Level One: JSON over HTTP
Are you building a JSON API for a project? Here's a high level guide to some industry standard practices to keep you on track.
If you're building a service that will be consumed by many clients it's a good idea to stay as RESTful as possible. If, instead, you're working on a Backend for Frontend (BFF) it normally makes sense to optimise towards having the minimal number of requests between the client and server.
This guide describes a REST-like API that returns JSON over HTTP which should fit both scenarios.
Summary
- Be consistent
- Use plural resource names
- Put the id in the URL if you're accessing a specific resource
- Use HTTP methods to manipulate resources
- Pick HTTP response codes responsibly
- Wrap JSON responses with a
data
key - Always paginate lists (and include next/previous links)
- Return errors in a consistent format
- Put the version in the URL
- Use headers for authorisation
Be consistent
Regardless of the advice below you should aim to be consistent across the API endpoints you offer. This is particularly important for things that either aren't defined in documents like this or don't have an industry standard that developers rally behind.
That could include:
- The use of
camelCase
vssnake_case
in field names - The use of hyphens in URLs
- How much detail you include in your error responses
- How tokens are generated for authorisation
- Whether you allow PUT, PATCH or both
- How you do pagination (page/page size, offset/limit, before/after)
- Managing versioning of the API
Create lightweight documentation for any decisions your team makes so that future developers can discover them.
Most importantly, new API endpoints should feel the same as existing ones. Clients shouldn't be able to tell when something was added to the service because of the way the API is implemented.
Separate your API endpoints into logical resources
Make your API easy to reason about by separating the endpoints into logical resources. Where applicable use domain modelling to define the resource in the language used by the business.
This is the general idea behind RESTful API design.
Your external-facing API doesn't need to use the same naming as the models in your code or tables in your database. Make sure it makes sense to the consumer, not just to the developers of your service.
Use the plural form as the base for your URL paths:
/v1/posts
/v1/meals
/v1/notes
Make the details about a specific resource available with the resource's id:
/v1/posts/:id
/v1/meals/:id
/v1/notes/:id
This pattern extends for sub-resources. Use the plural form and id for the detail view:
/v1/meals/:meal-id/ingredients
/v1/meals/:meal-id/ingredients/:ingredient-id
Use the HTTP methods to manipulate those resources
The standard HTTP methods let you perform the create, retrieve, update and delete (CRUD) operations that are likely to form your business requirements. Keeping to these standard operations helps to make your API predictable and consistent.
POST /v1/meals # create a new meal object
GET /v1/meals # return a paginated list of meals
GET /v1/meals/:id # return a single meal object
PATCH /v1/meals/:id # update a meal with the provided details
PUT /v1/meals/:id # replace a meal with a new object
DELETE /v1/meals/:id # delete a meal
Accept and return JSON
All responses should be returned with Content-Type: application/json
and a JSON body.
This includes both successful and error responses.
If you are adhering to a specification like JSON:API it's okay to use their Content-Type
instead.
There's much debate about whether response bodies should be "wrapped" in an outer object. Since we're going to suggest adding links
to the response body later, we're enforcing the standard wrapper from the JSON:API specification.
List responses:
{
"data": [],
"links": {}
}
Individual resources:
{
"data": {},
}
Error responses:
{
"errors": [{
"code": "ERROR-CODE",
"details": "There was an error in the core."
}]
}
Errors must always include a machine readable error code
and human readable details
field.
Don't include stack traces in error responses – that is leaking too much information about your internals. Instead include a correlationId
that lets you map the response to an entry in your application logs.
Request payloads should be valid JSON documents that represent the resource that is being changed. PATCH requests include a subset of fields to be updated and PUT/POST requests include the full object (except any auto generated fields).
Protect against changes with a version in the URL
Your API will change and some of those changes will almost certainly be backwards incompatible.
Adding the version as a prefix in the URL helps to make the API browsable in web browsers. Having the API version in the URL makes it easier to implement HATEOAS-style resource linking across versions if you decide to go down that path.
Make sure the version number changes any time you introduce a backwards incompatible change to an endpoint. It's up to you whether you want to bump the version of the whole API or specific resource endpoints when you make a backwards incompatible change.
A popular alternative is to use a request header to specify which version of an API a client is requesting. This can make sense if you have a very large API with lots of version combinations. See the Azure and Github REST APIs for examples of this in action.
Use headers for Authorisation
The HTTP specification defines the "Authorization" header (poor spelling for us British English folks) which should be used to authenticate requests to your API if required.
Use "Bearer" as a prefix for a token that has been generated by an authorisation server for a client to use. If you are using HTTP Basic Auth the prefix will be "Basic".
Always ensure that HTTPS is being used for authenticated requests. Your service should reject requests that include an Authorization header over HTTP.
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Avoid designs where tokens are passed as URL parameters or the request body.
Paginate list responses
Adding pagination to an API endpoint is a backwards incompatible change. You should include pagination for all list responses from the beginning.
How you paginate the response should be dictated by your data structures and business requirements. Simple paging of a fixed size is a perfectly fine place to start.
Use query parameters for the values you need for you pagination solution.
If you allow the consumer to change the page size make sure you document the minimum and maximum sizes. A handy reason to allow this is if you expect consumers to be interested in a page size of one.
GET /v1/meals?page=2
GET /v1/meals?page=3&page_size=10
When you paginate a response you should include links to the next and previous pages. Follow the JSON:API specification by adding a links
object in your response:
GET /v1/meals?page=2&page_size=10
{
"data": [],
"links": {
"next": "/v1/meals?page=3&page_size=10",
"prev": "/v1/meals?page_size=10"
}
}
Always return appropriate HTTP status codes
If in doubt the list is actually fairly small:
- 200 for when you're successfully returning something
- 201 after you've created a new object
- 202 the change will happen in the background
- 204 whenever there's no content to return (i.e. you've deleted something)
- 400 for when the provided request is invalid
- 401 missing or invalid authorization details provided
- 403 the user doesn't have permission to access this
- 404 what the client has requested doesn't exist
- 429 the client has been rate limited
- 500 something bad has happened
Think long and hard before straying from this list.
Don't allow HTTP requests
You should enforce HTTPS and not allow HTTP requests. If possible do this by not even listening on port 80 for insecure requests.
If you need to listen on port 80 then return an error for all requests. Avoid redirecting from HTTP to HTTPS for API requests. Clients may follow this redirection resulting in all payloads transiting the web without encryption.