Creating APIs for the human developer
How many times in your development career have you heard the phrase: “You should turn this into a reusable function” or “Please move this component out so we can all use it in our features”?
What does that even mean? What makes code reusable, and why should you even care?
It’s important to realize that whoever asked you to do this, is actually asking you to write an API. An API, Application Programming Interface, is a piece of code that allows programs to communicate, pass on commands or request data from one another. It is also an interface that provides abstractions for the human developer to more easily interact their own code.
By API, I mean the broader term. It could be a classic REST API, but also just a library or a tool such as jQuery, React, or MongoDB.
Extracting a piece of code and moving all feature specific logic out of it, is not enough to claim that you’ve built a good API. You shouldn’t worry about making it reusable; instead, you should worry about making it usable. When writing an API, we often forget that we write it for other human beings to use, and it is not uncommon to find a tool or a library that’s simply too hard to use.
I am Evyatar Alush, a front end platform developer at Fiverr. My job is to write libraries and tools for other developers to use. In my four years at Fiverr, I got to develop quite a few tools and APIs, both as open source utilities and for internal use. Some of the tools were good, and some were total garbage.
I’d like to share with you the lessons I have learned working on them, in my quest to build tools that developers will love to use.
The terminology can get confusing, so let’s try to make a few distinctions:
- Developer: You, the person writing the API.
- Consumer: Your user, the developer using your API.
- User: Your consumer’s user, the one actually using the end product. We’re not going to talk about them here.
Why should you care about building a good API?
Yes, you are building a tool that solves problems for people. They should all be thanking you, not criticizing your API decisions, right?
Well, not really.
I consider an API or a tool to be good whenever it does what it is supposed to do, without leaving its users frustrated and confused.
When people choose to implement a tool, they make an investment in that tool. If it is not good, it becomes ‘bad practice’ or ‘legacy’ (dev-speak for liability), but once the tool has been implemented, it can’t just be replaced or refactored out. Even if you fix your API, release a new major version with a brand new interface — upgrading/refactoring to receive a breaking change without a significant gain is an effort people (and companies) try to avoid.
So if your API is not good, or not easy to use, people and companies just opt not to use it.
On the other hand, a great developer experience leads to developers telling all their friends about your new and shiny API that you’ve worked so hard on.
What makes a good API?
There are many criteria by which the quality of an API can be measured. In my experience, there are three that are more important than others. I will try to explain each one in turn, but these three criteria are:
- It does its job
- It is intuitive and easy to use
- It is safe to use and hard to abuse
1. It does its job
Not much thinking here, that’s your API’s only reason of existence. If it doesn’t do its job, or even if it doesn’t do it great, it simply shouldn’t exist. Ideally, your API should do one thing, and do it well. Focus on one problem and solve it.
2. It is intuitive and easy to use
Intuitive: “Using or based on what one feels to be true even without conscious reasoning; instinctive.” -Oxford dictionary
I’ll take a small leap, and extend it to mean “Aligned with your perception of the world so much that it feels natural”. What does intuition have to do with API design?
A good API builds on top of knowledge the consumers already have. It should be simple for them to use because it is like things they have used before. Even if you are building something completely new, you can still rely on your consumer’s previous knowledge.
To be intuitive your API should:
- Respect the platform it is on
Your API should use similar terminology and conventions of the platform it is being developed for.
- Follow a pattern
When you write an API, you teach your consumers a new language. The different parts in your API should be consistent and match one another, to the point that a consumer would know what’s coming next.
- Don’t provide too many options
Your consumers will thank you for that. If it doesn’t have to do x, it shouldn’t do x. Not right away anyway. Keep the API as simple as possible, and extend it later whenever an actual need arises. It is much simpler to add later when you need it than removing it if you don’t.
Not providing too many options means your interface has a much smaller surface area, your consumers will be able to learn it much more easily and master it much more quickly.
3. It is safe to use and hard to abuse
As an API developer, your code reaches codebases that you may have never seen. Your API must not introduce bugs to your consumers, and sometimes you also need to protect your consumers from misusing your API.
Developers are human beings too. They are bound to make mistakes, they can get confused and they sometimes forget how to use stuff. It is your job as an API developer to help them make the best use of the APIs you build.
Designing for safety is not a simple task, but there are a few guidelines that should help you start:
- When dangerous, make it explicitly ugly
Usually, APIs need to be pretty and easy to use, but sometimes the power you provide the consumer is so great that it could be easily misused. To protect the consumer, you should make it so ugly and so explicit, that they would have to think twice if they actually want to perform the task they are trying to achieve.
- It should be runtime friendly
You never know who runs your code, and even worse — what other code is run alongside yours. Unless absolutely needed, you should never pollute global scopes, or take ownership over resources that are not yours, because you can’t be sure no one else is using them, so please — play nice with others.
- Handle errors intelligently
When designing an API for other people to use, you can never assume who is running it, or what they run it on — but most importantly, you can’t know what they rely on your API for. This means that you need to be very cautious about throwing exceptions.
There are many approaches to when you should actually throw an exception, and when you should handle errors gracefully. When it comes to writing tools for consumers other than myself, I try to take the more conservative approach.
If my API truly can’t function with the supplied input or configuration, I will throw a detailed error. Otherwise, I will try to avoid crashing the consumer’s program. In development mode, on the other hand, I will happily be throwing exceptions, allowing my consumers a fast route to error detection, instead of finding out something’s wrong when it is already in production.
Whatever approach you take, though, remember — a thrown exception is a part of your API’s contract with your consumer. You need to document when it throws an exception so your consumer knows how to gracefully handle the exceptions themselves.
- Prevent overrides
Or at least, limit as much as possible. Overrides usually happen when you leave some undocumented internal APIs exposed to the consumer. It could be an internal method or even just a CSS class name.
The more overridable your API gets, the harder it is to make safe, and the easier it is for your consumers to abuse.
It is nearly impossible to both have exposed internal interfaces and prevent your consumers from harming themselves.
Allowing your consumers to use the internal APIs means that you have no knowledge of their range of motion inside your API and when you create a new version, even patch or minor changes can easily become breaking changes for them, without you or them actually realizing it until it is too late.
How to design world-class APIs?
As developers we love shiny, super performant, ultra modular, highly optimized, clean code — but the hard truth is that usually, all of these don’t matter much.
Building a great tool is almost never an engineering problem, but a human engineering problem. What matters is not how it works on the inside, but rather how it works on the outside — the side that your consumers have to work with.
Of course, we should strive for performance and clean code, but as long as we have a solid API that our consumers are comfortable with, we can worry about improving the internals later.
There are three simple steps that I believe are the most important when designing your API. None of these is about your code:
1. Form your philosophy
Before you even start designing your API, you need to understand the problem you are trying to solve, and more importantly, which problems you are not trying to solve. This is the most elementary part of your design flow, and that’s your time to decide what your API does and doesn’t do.
Ideally, you should focus on one problem to solve, fully understand it, and not let any other feature sneak in. Many times you would want to add more features and capabilities to your API, just because “other people might use that”, but my rule of thumb is: if that’s not at the core of what you’re trying to solve, it shouldn’t be there.
Make some core decisions
This would also be a good time to make some core decisions about your API:
Do you use promises? Is this a sync or an async API? Do you mutate user data, or do you always return new objects? Do you use a class or plain functions? Static types or type bonanza?
Plan ahead your versioning and deprecation strategy
Your versioning strategy is the way you inform your consumers what is the scope of a change via the version number. It prevents them from accidentally receiving a breaking change.
Sometimes as an API developer, you wish to deprecate an existing API. It could be due to insecurities, bugs, missing features, or simply because of a strategy change. It is best not to abruptly remove an interface that consumers depend upon, and instead introduce the change via a deprecation strategy. A deprecation strategy is a way for you to inform your consumers ahead of time of a change coming their way which allows them to plan ahead.
You can, for example, log warnings about a breaking change. Or, instead of completely removing or changing an interface on a new major, you can retain the previous interface behind an opt-in flag, such as
--insecurewhich would explicitly enable the previous behavior until your users are able to refactor it out.
It is never about the internals, always the externals. The most important thing is that people need to be comfortable using your API. This is the time to experiment with different approaches to the interface. Don’t write a single line of code yet, just draft the use cases.
Let possible consumers see these drafts and ask them if they would find such an API comfortable, and iterate whenever you get feedback.
Do not let implementation details leak into the design
Unless it is impossible to work around it, don’t let the internal considerations impact the way the consumer interacts with your API. The API should always be an abstraction on top of your solution. You should never add more complexity to the consumer than you take away — don’t let them think about what’s going on inside.
If you let implementation details leak into the design, it will be much harder for you to refactor in the future, and you will be stuck with function names that do not reflect what’s actually going on.
Let’s assume you want to write a function that grabs user data from local storage:
But in the next version, you want to use IndexedDB with a fallback to localStorage. Well, you’re stuck with a bad name, have to maintain a legacy API, or introduce a breaking change, when you could have just named your function:
On the other hand, take a look at Preact, a near compatible implementation of Reacts API (with a few drawbacks), which is completely different internally. The way they were able to build a completely different project with almost the same API has nothing to do with Preact itself. They managed to do it because React’s team never let any implementation detail leak, and instead, they tried to focus on what is being performed, but never how it is being performed.
3. Put an effort into documentation
I know, people never read the documentation, so why bother?
Because they do.
There are usually two stages in which people go to the documentation of an API
- When they need a copy&paste quick start guide
- When something doesn’t work
If your documentation is not good enough or is not there whenever your consumers need it — it won’t matter how great the API is at its job, they just won’t have a way of knowing that. They’ll move on to the next library that solves the problem they have.
Errors can be documentation too.
Error codes and messages can help your consumers a lot when debugging their application. Give some thought to the errors you throw and give your consumers a helping hand by writing informative error messages.
Building tools and APIs for developers is a rewarding experience in which you both help others solve a problem and improve your own skills as a developer.
This article presents the puritan view of API design, but such an API can only exist in a vacuum. The truth is that API design is always a trade-off. You have the constant struggle between adding features and removing bloat — giving the consumer control and protecting them against harming themselves.
Finding the balance is not always easy, and you can never please everyone. As long as you remember that you are actually building tools for other human beings, who just happen to be software developers, you are heading in the right direction.