Skip to main content

Why API-first?

Deploying an API is like launching a satellite. Once it is in orbit, your ability to make changes is severely limited. If you are clever, you’ve added the ability to make orbital adjustments and small software upgrades (an evolvable API-design). But if you want to make fundamental changes, you have to launch another satellite and de-orbit the first. That's because once your API is being used -- changing it substantially will break your consumers.

The constraints of this kind of development are what lead teams to work API-first. Once an API ships and is being used by other teams you may have to support those interfaces for many years.

Your API is not your code. An API exists outside your code. If your codebase is a planet the satellite is in its orbit -- they influence each other, but they are not the same thing. An API is a promise you have made to consumers, code is how your fulfill it. You could rewrite your Ruby API in Rust and still keep the promise -- consumers would not be able to tell you changed anything from looking at the interfaces.

Satellite Engineers do not test their satellites by launching a new version to orbit each week, they iterate on the ground, where the stakes are low and experimentation is cheap. API teams work out the interfaces that make sense for their consumer use cases before publishing stable versions -- you can learn a lot by iterating with consumers before the launch.

Being API-first just means thinking about the promises you are going to make before you make them, and keeping the ones you have made. Eventually every API you put into orbit will reach the end of its life, when that happens you have to help migrate your consumers from one promise to another one.

That's why you should go API-first. The rest of this post is brief and tactical. It will outline the most important things to think about as your start working API-first with a team.

Build relationships with key API consumers

An API is well-designed if your consumers like it, and can quickly be productive with it. Once they integrate it as a dependency, it must be reliable and consistently do the job it was built for. That's it -- that's the definition that matters. Experts may tell you that the API "looks good", and they're probably right, but if users need different groupings of your data and different supported actions, you've failed.

That is why the very first thing we suggest to API teams is to go meet a few of their consumers and get to know each other. Invite other folks on your team and learn about each other's goals. Ask the consumers you build relationships with if they would be willing to review your API designs, and take them up on it. You may be surprised at how clarifying these conversations will be. You may discover things you thought were important that are not actually important to these consumers. You may realize there's a way to add a lot of value with very little effort, by building something you had not considered on your own.

You key consumers liking and consuming your APIs should be a metric that matters for your team. The definition of done for any API work should not be "shipped," it should be "consumers integrated the API and shipped it in their application". You are finished when you prove your consumers can finish their tasks.

Make good promises

Sometimes after hearing the satellite metaphor developers ask "so we have to get it perfect before we ship?"

No. There is no "perfect" and it is impossible to ship something that will never need to change. Shooting for perfect will slow you down and it won't help your business or your consumer's businesses succeed. But making a lot of bad promises you have to keep supporting for years is also risky.

I think the productive way to think about this is to aim to make good promises:

Define a clear and narrow use case for each API:

  • When would this API be called? By what kind of client?
  • What is the app/consumer trying to do when they call it?
  • What are the constraints do we impose? ie you can not cancel an order that has been delivered

Show that to your key consumers and ask them if it makes sense (this aligns you on goals). Then give them the API interface or mock and ask them if this API meets those requirements. You will learn a lot by asking those questions -- maybe the consumers do not have one of the values this API requires saved in the database, so they will need to query for it before running this API...maybe you can use different data to do the same task. It's those sorts of things you can only learn talking to the consumers.

Once you and 2-3 of your key consumers feel good about the promise -- ship it.

Review API changes

We have all experienced the benefits of doing code reviews. They give everyone on the team an opportunity to give early feedback and share knowledge. This leads to better software, fewer bugs, and a stronger overall architecture for our applications. Because we all have different backgrounds and perspectives, code review works well when everyone on the team is included.

Starting the practice of reviewing API changes has similar benefits on the quality, reliability and the developer experience of your API. You want to see engineers talking about API design in the comments whenever the API are changing. Optic makes API review easier by visualizing OpenAPI changes and letting them test each change for backwards-compatibility and other standards.

Intentional Versioning

If your API becomes a going-concern, eventually you will have to make some new promises. That is a good thing -- it usually means you have more consumers than you did before with diverging needs and diverse use cases. If you plan to run this API for many years, you should think about versioning up-front. You will need a strategy for changing promises, ending promises and making new promises.

Changing promises is ok, as long as you offer more, not less. You can change an API, as long as it is not a breaking change. Just like if you promise to call your parents once a month and start calling them twice a month they're still happy. You can make your API design easier to evolve by following some of the best practices. The reason people advocate for evolvable design patterns (like using objects at the root, keys instead of indexes, etc) is that they make the promises easier to change in the future. Being conscious of this up-front can make versioning much easier

Ending promises (deprecation). Sometimes you need to end a promise. Hopefully you have ways of knowing which of your consumers rely on that promise and can plan a deprecation and migration period with them -- if you can't, you should invest in some monitoring. There are large API companies like Stripe never break a promise and have never deprecated a single API, but that's expensive and may not be practice for your team. If you are going to deprecate APIs, communication is key -- make sure people know about it months in advance and present them with alternative APIs they can adopt.

Adding new promises is easier, but it comes with maintenance costs. Continue to be intentional about the promises you make, and how your API as a whole fits together. You do not want to end up with dozens of ways to do the same thing. Some teams split their API into versions to keep the latest promises in the same namespace ie /v1 or /2022-01-10, and previous promises in their own namespace.

If you work on an internal API with 1-2 consumers at your company, you might ask if you really need versioning. In some cases it is easier to make breaking changes and coordinate them with the other teams. To pull that off you need to instead invest in close relationships, building trust, and coordination changes ahead of time with those teams. Once the API is being used by 3-5 consumers this breaks down -- just know it will and be prepared to change strategies if what you are doing today no longer makes sense.

Hopefully that direction helps you get started. Being intentional about the promises you make, starting API review, and having a versioning strategy up-front will set your API up for success. It will help everyone on your team learn faster, together.