Oscar Robinson

London, UK based Software and Data Engineer. I love using Kotlin, Scala, Kafka, Postgres and Snowflake.

Designing Iterable HTTP JSON APIs

Having worked on backend systems at a start up for over 3 years, I’ve learnt a fair bit about getting code out of the door quickly. A lot of my work involved building internal HTTP APIs producing and consuming JSON. These APIs were only used by internal developers, thus could be rapidly iterated on to add new features. However, it is easy to back yourself into a corner when designing APIs. The main problem is if the front end clients calling your API cannot be updated in sync with the backend. This is often not a problem for web applications but is certainly a problem for mobile applications. Users may not update to a new version of the app for days or weeks, meaning your backend must be able to remain compatible with older clients for at least that long.

When you know the features of an endpoint may change rapidly, it’s important that the structure of the data it returns is flexible rather than brittle. Then, you can easily add new features without breaking older clients. Here are some tips I’ve learned to make the need for breaking changes less likely and also allow front end developers to build features quicker by providing a standardized API without inconsistencies in the most important areas.

Always Return an Object

Every endpoint you write should always return an object. Not an array, not a string, an object. Why? Here’s an example. You’ve implemented an endpoint to search some posts on the platform, let’s call it GET /posts/search. It returns an array of post objects:

[
	{ 
		"title": "A Story",
		"preview": "We can't bust heads like we used to, but we have our ways. One trick is to tell 'em stories that don't go anywhere - like the time I caught the ferry over to Shelbyville. I needed a new heel for my shoe, so, I decided…"
	},
	{
		"title": "Recommended Reading",
		"preview": "I found this cool blog post about API design"
	}
]

A requirement then emerges that the search posts page should also return a suggested alternative query if the search query looks like it may have a typo. All you need to do is return a string field with the suggested alternative query. But because you’re returning a flat array. There’s nowhere for this field to go. If the original response was an object, with a posts field, then it would be trivial to just add an extra nullable alternative_query field:

GET /posts/search?query=orznges

{
	"alternative_query": "oranges",
	"posts": []
}

This leads on to an important point for front end developers writing code to consume these APIs: Never write deserializers that cannot deal with unexpected fields in JSON objects, otherwise every addition of a field like this will require a version bump.

However, assuming this is the case, older clients can will just ignore the alternative_query field and newer clients can make use of it.

Of course, this kind of iteration can also be achieved with endpoint versioning, either by sending version headers with each request or using versioned URLs. However, having to add code to handle different response versions seems unnecessary here when it can be so easily avoided by just returning an object in the first place.

Return Requests With Standardized Metadata

Rather than returning the raw data for the request as a top level object as shown above, return a wrapper object. This allows you to return several useful things for each request. The most important being a request id. Creating a single unique id for each HTTP request in your API and logging it in every log message makes tracing the path of a request in logs a million times easier. By returning this id to clients, it allows for significantly faster bug resolution as client developers or even end users can just send you the id of a misbehaving request. This then allows easier log navigation to pinpoint the issue.

One idea is to return a top level object with a data and request_id field as below. This example is for the GET /posts/search endpoint described above. It also includes a standard next_page field which provides a page token to fetch the next page for any endpoint that returns lists of items.

{
	"data": {
		"alternative_query": "oranges",
		"posts": []
	},
	"next_page": null,
	"request_id": "f3012fe1-79bb-4fa2-8bf5-9addb7e2db56"
}

You could also have a standard error field that is populated when your endpoint is returning a non-200 response.

Page Tokens as Strings

I mentioned page tokens in the above point but another useful idea for these is to always return strings. For some endpoints you may want to page with an offset number, for others with an id of the last item you returned. For others you may even need a compound list of field values, for example if paging on something ordered by multiple fields. Either way, you can hide this detail from consumers of your API by standardising all page tokens in your API as a string. That way, client devs can code all their list views to simply expect a string in the top level next_page field and send that string as a page query parameter in the request to fetch the next page. Then if the contents of the page token need to change in the future for whatever reason, zero client work is required.

Error Codes

Showing useful errors to a user is a key usability feature in applications, and the ability to do that starts at the API. I’ve seen systems that base error messages in clients solely off the HTTP error code: For example a 403 response simply being translated to a generic “This action is not permitted” message in the UI. However, this is not always sufficient in situations where the user might need more info.

For example, say we have the action of adding a user to a group in a social application. If the call to do this fails with a 403 error, the user sees the aforementioned error. A requirement comes in to provide different messages depending on whether the calling user is not permitted to add users to the group or if the user to be added has been banned from the group. The easiest way to preempt such requirements is to start off returning decent error codes in the first place e.g

{
	"request_id": "f3012fe1-79bb-4fa2-8bf5-9addb7e2db56",
	"error": "USER_BANNED"
}

or

{
	"request_id": "f3012fe1-79bb-4fa2-8bf5-9addb7e2db56",
	"error": "REQUESTER_NOT_ADMIN"
}

The code in the error field allows the client to differentiate between the different causes of the 403 responses and show a corresponding helpful message to the user. Standardising the return of these codes to be in a top level error field means client developers always know to check and handle values in this field if its present.

You could in theory return the user facing error message directly in this field but that’s a bad idea as it will be a pain if you ever need to internationalize your app. Which leads onto the final point…

Never Return App Copy From The API

APIs for an app should never return copy for non-user generated content in your application. This should always live in the clients. For example, say you have a list of standardised configuration options that are returned from an endpoint because the contents of the list vary depending on your administrator role. Here each item should have a code rather than a title and description. The title and description live in the clients and are matched on the config option’s code. This way, when you need to internationalize your app, you only have to manage the copy in the clients rather than in your database or backend code.

Conclusion

Hopefully these simple ideas, all of which I’ve learnt from previous mistakes, will empower you to build APIs that are not only quick to iterate on, but also a pleasure for your fellow developers to use.