Why you should adopt API design-first principles
Why you should adopt API design-first principles
When constructing a house, you don't simply stack bricks haphazardly until it resembles a dwelling. Instead, you seek out an architect to create a well-thought-out floor plan, strategically placing windows, doors, and walls within the boundaries of your property. This sensible approach to architecture seems intuitive, yet, when it comes to software development and the resulting products we use, this mindset often gets neglected. The reason? Good API design is a challenging task, requiring flexibility to cater to diverse user needs, even when those needs seem to conflict. However, embracing the API design-first mindset can offer valuable insights, and in this article, we'll explore why it's essential for building exceptional software experiences.
What is API design-first
Arguably, API design goes beyond the scope of software development - you could also call it interface first design. It deals with designing and defining an interface with respect to the user before you consider how it is ultimately implemented. So it is about users and interfaces. Users are whoever is using the interface. In an iPhone, you are the user and the phone is the interface (not just the screen, the whole thing). In programming code, a library function is the interface and the developer is the user.
Design is there for a cause - usually to make a process or an interaction more simple, appealing, gratifying, efficient or quick. For example, when designing a house, you want it to be easy to navigate, comfortable to live in, and visually appealing. Similarly, when designing an API, you want it to be easy for developers to use, efficient, and effective.
So rather than starting with code and working your way up to an interface, API design-first starts with designing the interface and then building the code to support it. By focusing on the needs of the user and designing the interface first, you can create a more intuitive, user-friendly experience that is easier to implement and maintain in the long run.
Designing good APIs is not a walk in the park - it is a skill that needs to be learned. It requires experience in using APIs, having used the good and the bad, and ultimately knowing the specific domain as well as the greater context that an API deals with. If API design is not taking up a considerable amount of your feature implementation time, chances are you are not doing it properly.
Advantages of API design-first
So why should you start following API design-first principles - ultimately you’re not building a house, right?
Developer Experience
Especially, when building a product, developer experience will be a key element of your products success. The ongoing innovation in the market has constantly raised the bar of how simple scaffolding, testing and prototyping in software development can be. And APIs are no exception. Especially, if you consider what parts of your product are consumed by developers:
- CLI Commands
- HTTP APIs
- Web Hooks
- Extension Hooks / Events
- Templates
All these want to be designed with regard to naming, structuring, stability and intuitive handling. The resulting quality is often referred to as "API ergonomics".
Autonomy and Scalability
One of the early artefacts of the API design process is an API specification. The API specification describes the structure, behaviour, naming, produced side effects, preconditions etc. of you API. Sometimes also referred to as “API contract”. The API contract is an agreement that consumers and the provider of an API agree upon. That way, both of them can start implementing their side of the interface independently, because the contract clearly defines the boundary and expected behaviour. Teams that consequently follow the methodology of contract specification will have great benefits in their autonomy and scaling implementation efforts.
Ultimately, API design teaches you a lot about the underlying product. I like to think of a well designed API as “the distilled essence of your products business logic”. In other words - it separates implementation details from business logic. In turn, when an API leaks implementation details to the surface (e.g. through a parameter, a header or a response value) this is a sign of poor abstraction and potential points of instability.
Stability
Over its lifetime, every product and project goes through iterations of maintenance-related refactoring, bug fixing or new feature implementations. API design can help in multiple ways.
First of all, a well designed API is good to test. Ideally, tests for an API can be generated based on its specification, given it is sufficient in detail.
Secondly, it hides the internal part of the implementation behind a facade, so it can theoretically be exchanged without affecting “user land”. Of course in real world, response times (for example) can be part of an API contract as well, but again, these can be tested automatically.
Ultimately, you can never foresee all the features your product will have. So changes or breaks to an API are inevitable. But with an API design-first philosophy you are on top of those changes, because they will also change the specification. You break the API with intent - not by accident. So whenever the specification changes, you know that you have to transport these changes to your users - e.g. by deprecating an endpoint, introducing a new version, offering a grace period for support or providing examples in the response or documentation. Communicating a change through the specification is a lot easier than communicating it through the code.
If you don’t follow the specification-first approach, you risk to expose low-level changes to the API level and affect users without warning or you introduce undesired inconsistencies into your API surface.
Specificity
There is no one-size-fits-all approach to API design, but one pattern that I have found to be very useful is the concept of specificity.
As mentioned in the introduction, design fulfils a purpose. Apply that to API design and the purpose becomes whatever business problem the user is supposed to use the API for. That could be for example
- Extend a template
- Send a notification based on a business event
- Store or query data
- Create a new UI element
- Modify data
When designing APIs, I like to rank them amongst the spectrum of specificity. What exactly does that mean? The specificity of an API tells how “narrow” the definition of its use case is. Think of it as the opposite of “reusability” or “generality”. Some business problems are better served at a generic level, whereas others are better served with a more specific API.
For example, consider the following use case for extending a template:
As a developer, I want to extend the administration template to add a new button to the product detail page.
There are multiple ways to achieve that with an API. For example using an inheritance mechanism:
{% extends '@ControlPanel/module/product/product-detail/index.html.twig' %}
{% block page_product_detail_actions %}
{{ parent() }}
<div class="detail-actions__item">
<button onClick="alert('Hello World')">My Button</button>
</div>
{% endblock %}
This is a very generic approach, because it allows you to extend any template in the administration. However, it is also very brittle, because it relies on the template to be extended to be present in the first place. It also requires the developer to know the exact path of the template and the block to extend. Both are hidden within the internals of the template - and could change at any time. During updates, it is extremely hard to detect if the template has changed and if the extension is still valid.
Now consider the following API:
import { Admin } from 'extension-api';
const actions = Admin.get('product-detail').get('actions');
actions.add({
label: 'My Button',
events: {
click: {
notify: {
message: 'Hello World'
}
}
}
});
Which one do you like better? Maybe that's a matter of personal preference, but we can certainly make some objective statements about the two approaches:
Inheritance | Extension API |
---|---|
More generic and allows you to extend any template | More specific to the use case / business problem |
More brittle, because it relies on the template and block to be present | More stable, because it is not affected by changes to the template |
Requires the developer to know the internals of the template | Does not require the developer to know the internals of the template |
Difficult to provide sugggestions and auto-completion | API can be fully typed and documented to provide auto-completion |
There are some correlations that we can draw from this example (which generally hold true):
- The more specific an API is, the more stable it is
- The more general an API is, the more knowledge it requires from the developer
Conclusion
In conclusion, API design-first thinking offers valuable insights that benefit both API developers and API users. By prioritizing the needs of the user, we can create better products and enhance the overall software experience. While there's no one-size-fits-all approach to API design, applying the principle of specificity allows us to prioritize business problems effectively and strike the right balance in our API design.
It's common for developers to lean towards generic APIs out of fear of missing use cases, but the reality is that specific APIs are more stable and less prone to misuse. So, instead of shying away from specificity, we should embrace it confidently to build robust and well-crafted APIs that truly cater to user requirements.