Common API mistakes and how to avoid them
API development may seem simple, but designing ergonomic APIs that are a joy to use is a subtle art. In this post, I will dive into common mistakes that I’ve encountered.
About me: I’m a backend engineer in the integrations team at Pento (we’re hiring!), and as such, I work a lot with third party APIs. I’ve compiled a list of 7 common mistakes to share with you.
1. Poor documentation
This is an obvious one, right? No matter how awesome our API is, if there is no documentation describing how to use it, nobody will enjoy working with it. Even if it’s written in a very intuitive way, when (and not if!) a developer doesn’t find something obvious, they will need to refer back to the docs, so it’s better to just document everything clearly.
One of the most common mistakes I’ve seen is neglecting to describe the expected values of inputs and the return values of outputs. For fields that have a restricted set of inputs or outputs, it’s best to always list all possible values.
More complex requests can be hard to implement properly, even with good docs. In these cases, I found cURL commands and code snippet examples most helpful to get started playing with right away.
Tip: Decreasing friction in DX drastically improves the perceived quality of your API.
Oh, and do I need to mention outdated documentation?
2. Lack of a proper permission system
A lot of APIs implement customer-selectable permissions for each API key (as they should), but don’t expose any endpoints to query granted permissions. This is problematic in cases where the customer doesn’t grant permissions to endpoints used by the consumer of the API. Instead of querying available endpoints to use, the developer can only rely on HTTP status codes or errors returned at runtime.
At Pento, we implemented a “connection test” to ensure that our systems can access and query the necessary data from the APIs we integrated. This involves calling all endpoints of the APIs that we use to verify that our customers have granted all required permissions.
This is rather cumbersome and leads to a lot of problems, such as unnecessary load on the server and the client, wasting the (often low) rate limit and having more code to maintain. Write/modify operations are also very hard to test this way.
This is why it’s important to make sure that if we have customizable permissions, also have an endpoint that can be used to query the permissions granted for the API key used.
Lastly, it’s important to be consistent with the permissions. I’ve had to use an API where even though having been granted read permissions for an object, I could not see all the data in the response. It was only after contacting the developers of the API that I found out the fields containing PII required write permission to read. ¯\_(ツ)_/¯
3. Unreliability
It does not matter how good our API is if it’s unreachable half the time. In 2023, there are no excuses for downtime. The consequences of an API not being available don’t stop at being unable to reach the required resources. When the consumers are written poorly, unavailability could lead to crashes, or even loss of data.
In some systems, APIs are called synchronously with user actions, and an unavailable API could lead to UI errors or worse, a system left in an invalid state. Assume that most, if not all consumers will be lazy to implement a retry system.
While uptime is paramount, don’t forget about response time either. It’s best to try to keep the hot path of the code short and rid of heavy or external calls such as long-running SQL queries or external API calls. Aggressively cache whatever you can afford to.
With the obvious problems cleared up, here are a few pointers on what else we should avoid:
- Breaking changes to the API: just don’t do them. If you do them anyway, version your endpoints.
- Purging keys: unless there is a security concern, don’t revoke keys without notifying the users. You never know what system may depend on them.
- Shiny new tech: they will just break your system, and you will have a long and hard time finding answers on StackOverflow. Boring technologies are our friends.
4. Gaps between UI and API
This is probably another one I don’t need to expand much on. Features missing from the API that are present in the UI make it hard if not impossible to use our system through its API.
5. Inconsistent interface
Consistency is key. Our APIs should aim to stick to a single convention throughout the whole interface that is exposed. As an example, in a REST API, error handling should be either done through HTTP response codes, or in the body of the response with a fixed response code.
Tip: in HTTP, almost all things are standardized even though the rules are rarely enforced.
HTTP request methods: they actually have a standard meaning that should not be reinterpreted. GETs are for getting data, POST for sending data, DELETE for deleting entities, and so on. I’ve seen APIs use exclusively POST requests for no apparent gains. When these are used logically, it increases DX and decreases the need to rely on documentation.
URL paths vs query parameters: Paths are for mandatory data like IDs. Query parameters are for optional fields. Mixing these could lead to confusion.
Response codes: if we were to ask any stranger what a “404” is, chances are, they would say it’s a “not found error”. This is because the codes actually have strict meanings, and though nothing will break by not following the standards, this actually increases the cognitive load on the dev using our APIs.
Error handling:
- Always send errors with non-200 response codes. A lot of devs often just check for the response code being between 200 and 299 before parsing the body of the response.
- Error handling should be uniform across all endpoints. This is something I’ve often encountered, where endpoints added later to an API don’t conform to the same formats.
Sending random values: Most modern APIs use JSON, which is a convenient, but not strict, format. It’s best to stick to a (sensible!) convention and apply it to our whole codebase. Reconsider sending floats in quotes.
6. Unusable data formats
Although there are countless possible technologies that could be used to put together our API, in practice, it’s better to stick to the basics and go with a modern, well-supported data format that has utility libraries for both sides. 99% of the time, this will be JSON. In new projects, there is no reason to use obscure or sparsely supported technologies.
Tip: always stick to industry standard tech and tools. Relevant XKCD.
We might resort to paging if you have too much data to return. This is a good solution, but it’s important to keep in mind that they can be hard to use. Never mix non-paginated and paginated responses in one endpoint and make it clear in the documentation which endpoints are paginated. Pages should include current offset and page count.
A lot of APIs I’ve encountered also returned redundant information, responding with the same data — although in different formats, making this problem harder to realize — in multiple endpoints. Just like databases, APIs should return normalized data that can be used to reconstruct whatever was sent over the network.
A few quirks we should definitely avoid:
- Dates in non-machine-readable formats: APIs are not made to be consumed by humans. Use standard formats such as ISO 8601. The same thing goes for times, which should also include timezones.
- Non-standard headers: they are a chore to parse, and no developer will think to look at them if they’re not documented.
- “Tabular data”: some APIs expose a part of their data in a different format, making the parsing of API harder.
7. No proper versioning – “stable interface”
This is one of my favorites. Some APIs explicitly state in their docs how their interface is “stable”. These are also the very same APIs that will pull the rug from under us on a regular schedule.
This is a promise that we will not be able to keep. Even if we’re absolutely sure we will never add breaking changes, a good practice would be to just slap a /v1 somewhere in the path anyway.
7+1. No clearly defined rate limits
In my experience, this is one of the most poorly documented things in an API, even though it’s a crucial feature that could break the consumers of it if they are misconfigured (e.g., misconfigured internal rate limiter + no retry mechanism).
Important things to state in the docs:
- What endpoints are rate limited?
- What is the speed of the rate limit (requests/minute or hour)?
- What is the scope of the rate limit (per API key, user, IP address, or something else)?
- What do rate limited responses look like (important, so they can be handled explicitly)?
Afterthoughts
I hope this was an informative article and I could help to make your APIs easier to use in the future. Don’t hesitate to reach out to me with any questions or thoughts!