It’s interesting to note that as the trend in programming languages is towards more static, stricter typing and away from dynamic loosey-goosey types, the trend in data interchange has gone the other way.
Back in the day we had XML, a markup language which was not a great choice for data interchange, and things like SOAP built on top of that. These formats were difficult to work with and error prone, so they were rightfully replaced by JSON.
While this was a welcome change, we did lose some things. In particular, we lost schemas and strict typing. JSON is very loose about types – there are only a few supported data types, and they don’t map to the ones used in most statically typed programming languages. There has been some work to develop schemas for JSON, but I’ve yet to see them used in the field.
Just as with a dynamically typed language, this looseness makes working with JSON very easy for rapid development. However, it does come with a responsibility, and that is to document and to test your code. As hated as XML was, at least it forced developers to think about types and how to represent data.
Over the last few years, I’ve worked with enough poorly designed JSON-based web APIs that I know this is a problem. But just as you can be careful about types in a dynamic language, you can be careful about types in JSON. It simply takes extra diligence. Here are some basic rules you can follow to ensure your JSON-based API doesn’t suck.
Use a Framework
The simpleness of JSON gives people confidence that they can roll their own API. This usually results in something underspecified, nonstandard, and subpar. Given the proliferation of REST frameworks available for just about every server language, there’s no excuse for this. Not to mention tools like Swagger.
Along, the same lines, great thought and research went into the REST concept. Notice how above I made a point to say “JSON-based web API” rather than “REST API.” Despite popular perception, there is more to REST than JSON and HTTP. Take the time to learn about the concepts and why they’re important, and use them.
A good start is Roy Fielding’s dissertation on the topic. You may choose not to follow all of REST, but make that a conscious choice, and not a default based on ignorance.
Or better yet, follow my above advice and use a framework.
Document your types
As a user of your API, don’t make me guess. Is this number an integer? A double-precision floating point? A binary-coded decimal? Are there upper and lower bounds on its value? Document it.
How do you encode dates (and time zones)? Binary data? References to other objects? Document it.
Can this field be null? Document it. Can this field be omitted completely? Fine. Document it.
Is this field a Boolean value? Send it as a Boolean value. DON’T SEND IT AS A STRING.
Is this field a number? Send it as a number. DON’T SEND IT AS A STRING. (A possible exception to this is a binary-coded decimal number. Document that.)
Once again, there are server-side REST frameworks that will handle most of this for you.
This seems elementary, but experience shows it needs mentioning. Understand the different HTTP request types and how they are used. Don’t change data in response to GET requests. Don’t take query parameters in your POST requests.
Along that line, document how POST data is sent. Form encoded? JSON encoded request body? I don’t care which you use, but document it.
Take advantage of the “verbs” that HTTP offers, so you can make your endpoints represent objects rather than actions. For example, instead of having endpoints called “/getEmployee”, “/updateEmployee”, “/deleteEmployee”, etc. go with one endpoint called “/employee” with an optional ID segment (like “/employee/123”), and allow GET, DELETE, PUT, and POST actions to it.
Learn about HTTP response codes. The “200 OK” response means a successful call. Anything in the 400 range is an error in the request. And so on.
Document the Errors
Errors can happen, and they can be unpredictable. It can be a lot of work to catalog every possible error that a service can return for each object, but give it a shot. If that’s impractical you should at least define a standard format for error responses. And as mentioned above, use the correct HTTP response codes so the client knows that a response is an error even if they can’t parse the details.
It is not important whether you use snake_case or camelCase or whatever for your field names. But you should pick one and use it consistently throughout your API.
And try to maintain consistency in other ways across objects and endpoints. For example, if your /cloud/ endpoint contains a field called “cloud_id” then your /bird/ endpoint’s ID field should be called “bird_id” and not “birdID” or just “id”.
These are simple ideas, but surprisingly challenging to get right, especially when you have multiple developers working together. But taking the time to get it right makes working with the API much much nicer.
Go easy on abbreviations. Since this is a network-based protocol, it is fair to think about bandwidth limitations, but as in all programming contexts there are risks associated with abbreviations. The biggest risk is that what may seem like an obvious choice to one developer may not be so to another. For example, one may use “acct” to stand for “account” while another may go with “acnt”. These types of inconsistencies slow down development for the user of the API.
My preference is to avoid abbreviations unless they are industry standard, such as “i18n” or “oauth”.
Speaking of OAuth, one exception to the consistency rule is if you’re integrating a third-party framework. In OAuth, for example, there are standard field names such as “access_token” that are expected to be in snake_case. Even if the rest of your REST is in camelCase, stick to the standard for OAuth endpoints and the like.
The JSON standard allows any arbitrary string to be used as a field name. But just because you can doesn’t mean you should. In using one poorly-thought-out API I came across a field name of “$value” to represent a price. Perfectly legal JSON, but I was using a framework that mapped JSON field names to member variable names in a Swift struct, and I had to carve out a special case just for that one value. That means more complicated code and more room for error.
Know the Prior Art
REST has been a de facto standard for a long enough time that there are plenty of examples you can reference for how to do it. A quick Google search turns up several directories listing them. Before starting on yours, read through some of what’s already out there.
Pay special attention to the ones that have gone through several versions, as they have probably worked through problems that you’d be likely to encounter.
Also, if there is a service that has similar functionality to yours, that can be a good starting point.
Some well-known examples that are also well-done are GitHub and Facebook. (But resist using GraphQL until you know you need the optimization.)
Understand Authorization and Authentication
Security is a complex enough topic that I won’t go into too much detail here, but spend the time to understand it and implement authentication and authorization using one of the well-tested standards like OAuth. Or better yet, use a REST library that handles that for you.
If there’s a recurring theme here, it is that you should not reinvent the wheel. As in many areas of computer science, there is enough prior art here that there is no excuse to do this badly. Do your research and decide to make a good API.
And of course (say it with me) document it.