There's no debate around the fact that good API design is an art. When we stumble into a properly designed API, we can feel it. Just like good visual UIs, good APIs are not just beautiful, it's functional and it saves everyone's time. With this mind, it's not a stretch to say that APIs are, in fact, UIs. They're not just meant for machines either: since there are people programming and using those machines, creating an API that users are delighted to interact with is a deliberate choice of its development team, and one we ought to make.
There are many resources about design principles for visual UIs, as well as a lot of good examples to take inspiration from. When it comes to APIs, on the other hand, there is a lot of documentation explaining the different protocols, languages and frameworks for their implementation, but nearly not enough material on the underlying principles and choices that make certain APIs a joy to work with and others a complete nightmare.
While nothing can beat experience and each team comes up with their own rules for what makes an API great, we can all benefit by applying some basic principles to the design of our APIs. These are very abstract and can be implemented in many different ways, but they should always guide our technical decisions.
Consistency means that similar endpoints should behave in similar ways, including in edge scenarios. You should always strive to keep vocabulary, URL structure, request/response formats and error handling consistent across your entire API.
This provides several benefits:
- It makes it much simpler to interact with your API, since users always know what to expect and don't have to read the documentation of every endpoint, but can simply go with their gut.
- It allows writing client libraries that don't know the exact schema of the API, just the rules you commit to respecting. The Stripe API client is a good example of this: because the structure of requests and responses is always the same, it can build objects dynamically and only needs to be updated when one of the API rules changes.
- It creates a set of battle-tested guidelines that you can abide by when you are implementing new features in the API. No more discussions around whether to use numeric IDs or UUIDs, because you'll just go with whatever you already have in place.
Consistency is probably the most impactful trait you can implement in the design of your API, and your users will love you for it.
Performance in HTTP APIs is a tricky matter. Because APIs are not directly consumed by end users, it's easy for performance issues to go unnoticed for a very long time, especially in the context of server-to-server interactions. Unfortunately, because software is made for people, all types of performance issues will eventually impact the end user.
I strongly advise against early optimization, for the usual reasons: it will slow down the development of your MVP and you can't know what parts of your product need to be optimized without having the right metrics in place first. You should optimize based on data, not instinct.
With that said, it is extremely important that you start collecting the data from day one. Setting up an APM tool is a matter of seconds nowadays, and it will provide you with a ton of useful information about how your API is being used in the real world.
No matter how consistent your API is, users will still need a place they can go to in order to get started or obtain additional details about certain aspects or capabilities of the API.
Furthermore, API documentation should not just be about presenting request and response templates. It's useful and recommended to add any information your users might find helpful, such as explanations as to what happens in the background when a certain API call is made or what other endpoints the user might need to complete the transaction at hand.
It is extremely important that your API documentation is accurate and up to date. The only way to accomplish this is by creating a workflow to integrate documentation into your development process. A good way is to check documentation into your VCS and require developers to update it whenever they change the API.
Having developers write documentation might seem like a waste of time, but the more time you spend documenting, the less time you'll spend answering questions and investigating bogus bug reports.
Finally, ensure the documentation is not just available, but a joy to consult. Since it is meant to be consumed by humans, put extra care into the UI and UX of reading the documentation. Instead of doing this yourself, you can just adopt a service such as Stoplight that will store and present the content in a standard format for you.
While there is nothing inherently wrong with having a 1:1 mapping between your database tables and your API resources, you should know that this is not the only way to build an API. In fact, for complex APIs, it's often not the best way, because it puts all the burden of stringing the right set of operations together in the right order on the user's shoulders.
If you can simplify a business transaction to only require one API call instead of two, why not do it? It would be unthinkable for a visual UI to require user input for every single operation that happens in the background - and yet, that's exactly what we do in many APIs, when we ask our users to do our work out of laziness.
One example of this is an API that allows users to add a new payment method and mark a payment method as the default one. If we expect situations where the user wants to add a new method and immediately mark it as default, we can add a mark_as_default attribute to the payment method creation endpoint and do everything in one transaction without requiring two API calls. This saves both development time and bandwidth.
This kind of simplification is not always possible: when you are not able to determine the exact use cases of your API in advance (e.g. in the case of public APIs), you ought to be as flexible as reasonably possible (in the example above, this would mean adding the field but also maintaining a separate endpoint for setting the default payment method).
The rule of thumb is to consider all aspects of your API as means to an end, i.e. to complete a business flow. How can you best help the user achieve that goal? Once you have that in mind, design choices become much easier.
Build your API on top of simple, universally accepted standards and tools. There's no need for envelopes, schemas, API gateways or any other esoteric solutions unless you have very good reasons for using them.
The HTTP RFC already provides you with most of the tools you'll need to build a Web service that is reliable and interoperable, so go ahead and read it (yes, it's long, but I can guarantee it's worth it).
Stick to the basics and do them well. Simplicity means less overhead for humans and machines, as well as less room for mistake. For instance, here's what it takes to charge a credit card using the Stripe API:
curl https://api.stripe.com/v1/charges \ -u your_api_key: \ -d amount=999 \ -d currency=usd \ -d description="Example charge" \ -d source=tok_visa
It's so simple you can do it without looking up the documentation, and it's virtually impossible to mess it up. This is the kind of simplicity you want to strive for.
(By the way, the Stripe API is an excellent example of thoughtful API design, so take a look at the API reference if you have some time.)
Traditional Web applications are updated all the time: new features are designed and added, popular ones are improved and streamlined, unused ones are deprecated and removed. Users are accustomed to living and operating in an environment that's constantly changing, sometimes quite dramatically.
Most of the time, products do a very good job of nudging users to adopt new features: newsletters, in-app tours and good design practices ensure that the UX is never disrupted by the latest round updates.
For APIs, on the other hand, it looks like there are only two possible options: either you never change your interface for fear of breaking implementations, or you change it without notice, disrupting the most of your customers' work. This happens because API teams are often disconnected from their user base, since they don't interact with users directly.
But a third option is entirely possible: with the right infrastructure and tooling in place, you can have an API that changes in a way that is manageable for its users. You can use metrics to make informed decisions about the interface you expose to the world: how often is an endpoint used? What users called a specific API endpoint in the last 6 months? Make sure you can always answer this kind of questions, because they will allow you to evolve your API without making your users' day a nightmare.
Over to You!
Do you have any other principles that you apply when designing APIs? Or have you seen examples of very well-designed APIs? We'd love to hear from you and discuss, just leave a comment below!