I really enjoy talking about REST API. Yes, for some people, REST is the past-generation stuff, and GraphQL is the cool kid in the block. Even though over the past months I've used GraphQL every day, REST still has my heart, and I have to be careful to avoid my bias toward it.
If you had to use SOAP as a communication protocol, REST was most likely your savior from the deep trenches of old-school distributed systems. Sadly, even though I'm not that old in the area, I've worked for an old bank, which exposed me to all kinds of ancient things you can imagine (yes, even COBOL).
This article is not a extensive guide on best practices, is just some quick thoughts that might benefit beginners in the REST journey. If you're an experience developer, this will most likely be boring.
Goals
One of the most important subjects to understand how to make your API comprehensive to your users/consumers is to have clear goals. In his book "The Design of Web API's" Arnaud Lauret defines five questions that one needs to answer to start reasoning on how to deliver a proper API.
- Who can use the API?
- Here's where you list the profiles of people who are going to use your API;
- What can they do?
- How do they do it?
- Where you will decompose the previous question into steps
- What do they need to do it?
- What is needed in each step, and where does the input comes from
- What they get in return
- Which is where you state everything that is returned by each step and how it will be used;
- Goals
- Where you will combine the hows, inputs, and outputs into a concise objective
Answering these questions will enable you to clarify what are the things your API must deliver. You can define a table where you fill those things, and you can run through a product manager and other colleagues to get feedback on any spots that you might be missing.
Error handling
Identify the possible errors of your API. Document all the cases, and error codes that you might return. A really nice to have is exhaustive errors. It's really annoying when you send a request, then the consumer receives an error. When the error is fixed and you re-submit the request, you get a new error. You can easily a errors
array and add all the issues into a single response, so the consumer can clearly see all the issues within the request. This is especially needed for API's that are used in forms.
A good pattern I've found is to return something along these lines:
{
error_code: SOME_ERROR_CODE,
error_message: 'Something wrong is not right',
validations: [{
property: 'entity.created_at',
code: 'INVALID_DATE',
message: 'whatever message'
}]
}
Be predictable
Lauret has a definition of consistent API's that I quite liked it. He splits into 4 levels:
- Consistency within an API
- Consistency across an organization/company/team
- Consistency with the domain of an API. How you represent the entities should be matching the domain experts' expectations
- Consistency with the rest of the world. Meeting standards, like ISO's, RFC's, and protocols.
So, be consistent wherever you can. Entity names, API responses (including error ones), path, headers, etc.
Before I forget, it would be lovely if folks stopped using
X-
for custom headers. There's an RFC deprecating this for around ten years now. I don't blame you, no one talks about this.
Adaptability
I loved when I worked in an API where we enabled our customers to get our reports in whichever format they wanted. JSON? You have it. PDF? Right now. CSV? Go crazy, just send me the header.
Here you can also include localization, internationalization, etc. I mean, your users might still use a really old imperial system that doesn't make any sense at all. If you send them a response in meters they will be completely lost, so you could enable them to have the answer in yards.
Your REST API can also enable lazy load to improve performance for your customers, they can easily expand the data sets they're interested at from a given entity. This is one of the main selling points for GraphQL from many folks, but can also be done in REST API's. Stripe API for reference.
Pagination
Please, pretty please, don't return something that only gives the consumer the next_page_token
. This doesn't enable the consumer to go back and forth on the data.
You can have a quite simple response and still be useful:
"pagination": {
"count": 10,
"total": 250,
"current_offset": 10
}
Then you know that the next page has the offset parameter as current_offset + count
. A good thing is to have a parameter to limit the number of elements returned as well.
Documentation
One of the main things, and often overlooked, is to have clear documentation. For me, one good example is the Stripe API. They provide not only clear documentation but easy-to-follow examples, with commands that you can copy and paste to try it out. From my humble perspective, this should start to be in place from the moment you start designing your API. Otherwise, details will often be overlooked later on.
One other relevant portion of your documentation is that you should avoid the trap of only having it in an "engineer-oriented" format. It's always beneficial to have it in a human-friendly format (your JSON Schema is not so human-friendly).
Versioning
When applying semantic versioning to your APIs, all that matters are two digits: BREAKING_CHANGE.NON_BREAKING_CHANGE
. However, your customers don't really care about the non breaking changes. So you can use the versioning only on the first value.
There are many ways you can version your API. Path, domain, query parameters, custom headers, content negotiation. Pick your flavor. I honestly tend to prefer the version to be in the path, such as /v1/users
.
Request tracing
Use correlation_id
's across your requests to enable yourself to trace the request flow across multiple services. If you're not familiar with the concept, you can read it in here.
You will be thankful when debugging.
Idempotency Key
Your customers could have retry operations when things fail, automatic reprocessing of messages. This could end up creating problems if there's no way to check if a operation has already been done. Idempotence is the property where an operation can be done multiple times without affecting the end result beyond the first application. Enabling your customers to take control over their idempotent requests is super useful. Stripe API has a good example on it if you want to dig further