Brenton Cleeland

Level One: JSON over HTTP

Published on

json-over-http.jpg

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

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:

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:

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.

Further Reading