API Versioning is Not Application Versioning
Your API is a promise to your clients. Try not to break it.

Search for a command to run...
Your API is a promise to your clients. Try not to break it.

No comments yet. Be the first to comment.
Let's go back to focus

Hope Is Not a Strategy

I haven't posted anything in a month, and I'm finally coming back to it. It feels as if February flew by. There have been a few projects at work that required additional attention, and I think they to

Dependency Inversion Principle, Dependency Injection, Singleton.... What?

API versioning sounds like one of those topics that should be pretty simple.
Put /v1 in the URL. Eventually make /v2. Congratulations, you have versioned an API.
Except that is not really the important part.
Before talking about API versions, we need to understand what an API actually is, what a client is depending on when it calls one, and why seemingly harmless changes can cause somebody else's application to start throwing exceptions
The important part of API versioning is not the version number, it is the contracts involved.
At its simplest, an API is an interface that allows one piece of software to interact with another piece of software. Think of it as the doorway into the capabilities provided by an application.
Imagine I build an application for managing a hockey team.
Maybe it keeps track of players, games, standings, goals, penalties, and which defenseman said they'd show but never really did, and now everyone's confused about why we're a player short!
Another application could interact with it through a RESTful API with something like this:
GET /players/88
And receive:
{
"id": 88,
"name": "Patrick Kane",
"position": "RW"
}
More than likely, there's some sort of front end application that is making the calls to this API. There might be a mobile application. But there might be another backend service calling it.
There might be absolutely no other way to interact with the hockey player application.
Sometimes developers think of the API as something bolted onto an application. In many modern systems, the API effectively is the application's external interface. The business logic, database, frameworks, and infrastructure sit behind it.
The API defines how other software gets access to those capabilities.
That interface creates a contract between the application providing the API and the applications consuming it.
When we say API contract, we are talking about much more than a URL.
Let's go back to the previous request:
GET /players/88
Authorization: Bearer someToken
And this response:
{
"id": 88,
"name": "Patrick Kane",
"position": "RW"
}
There are already quite a few pieces of the contract hiding in this tiny example.
The client knows that the resource exists at /players/88.
It knows that it should use GET, it knows authentication is required. It knows the response will be in JSON.
The response body will have an id. It knows there will be a name. It knows there will be a position.
It probably knows what HTTP status codes to expect when something goes wrong.
Those expectations are the contract.
A typical API contract includes things like:
The URI of the resource
The HTTP methods that are supported
Request parameters
Request body formats
Response formats
Data types
HTTP status codes and error behavior
Authentication and authorization requirements
The meaning of the data being exchanged
The implementation behind that contract can be completely different.
That gives us an important architectural boundary.
The client depends on the contract, nothing else.
The client doesn't care about the implementation. Those details are abstracted away.
You could rewrite the entire application. You could change frameworks. You could change databases. You could change your data source from a database to a text file.
You could replace one gigantic service with several smaller services.
You could discover that the original developer made some truly fascinating architectural decisions at 2:00 AM and replace half the application.
If the contract stays the same, your clients may never know, and shouldn't care.
That is a feature.
Words like service and module are so generic that they eventually mean nothing. The term client in this case can sometimes causes confusion, especially in this context.
People hear "client" and immediately think about some external company paying to use an API.
That is certainly a client. But it is not the only kind. If another application consumes your API, that application is your client.
It does not matter if both applications belong to the same company. It does not matter if both engineering teams report to the same manager. It does not matter if the developer consuming your API sits six feet away from you. Think of it more in the HTTP Lifecycle style of client/server architecture.
Suppose we have a Hockey API:
All three systems are clients.
They may all operate in the same building, same server, built by the same team, but breaking the API still ruins somebody else's night.
Internal APIs deserve contract management too.
It is possible that ignoring contracts on internal APIs can be particularly dangerous because teams sometimes assume they have more freedom to change things and do what they need to get the job done.
"We own both applications."
Great.
Does that mean both applications are always deployed simultaneously? Does the same team maintain both of them? Do you know every application currently calling the API? Will you still know that three years from now?
Probably not. It sounds like tight coupling to me.
Internal does not mean dependency free.
Now imagine somebody decides this field name is not very good because naming things is hard:
{
"name": "Patrick Kane"
}
They clean it up:
{
"playerName": "Patrick Kane"
}
That seems harmless.
Maybe playerName really is a better property name.
The change passes all the unit tests. The API deploys successfully. Everything looks fantastic. WORKS ON MY MACHINE!
Meanwhile, somewhere else:
const playerName = response.name;
Whoops!
From the perspective of the API developer, one property was renamed.
From the perspective of the client, the contract was broken.
That distinction is the entire reason API version management exists.
Software changes all the time. An API should not receive a new version every time the software changes.
A bug fix does not require API V2. Refactoring some business logic does not require API V2. Updating your framework does not require API V2. Changing your database does not require API V2.
The important question is:
Did I change the shape of the doorway my clients use? Did I change the contract my clients depend on?
This brings us to backward compatibility.
Some changes allow existing clients to continue working exactly as they did before. Say we add a new optional query parameter:
GET /players?position=RW
Existing clients do not have to use it. Nothing broke. Maybe we add a completely new endpoint:
GET /teams/17/schedule
Again, existing clients can happily ignore it. They don't know about it and they don't care. Maybe we add an optional property to a request. Existing clients can continue sending the same request they sent yesterday.
Those are generally backward compatible changes. Now consider some different changes.
You rename a response property. You remove a response property. You add a new required request property. You change the structure of a response. You change the meaning of an existing value. You've changed the doorway significantly. Now existing clients may need code changes.
That is a breaking contract change.
A useful mental model is this:
This is not a perfect universal rule. Software architecture rarely gives us those. It is a very good question to ask before deciding that you need another API version.
There is another trap here.
Once teams discover versioning, sometimes everything becomes a new version.
V1, V2, V3, V4....
At some point you have created the API equivalent of keeping every hockey jersey you have owned since you were twelve because you might need one of them again.
Every API version has a cost.
If clients are still using V1 when you release V2, you may need to support both.
Now bug fixes may need to be tested against multiple versions. Documentation needs to explain multiple versions. Monitoring needs to distinguish between versions. Developers consuming the API need to understand what changed.
Eventually the client will need to migrate to the new version of the API.
This is why backward compatibility matters so much. The goal is not to become really good at creating API versions. The goal is to become really good at evolving an API without unnecessarily creating them.
Eventually, you will have a legitimate breaking change. When that happens you need some way for identifying versions.
One common approach is including the version in the URI:
/api/v1/players
/api/v2/players
There are other approaches.
You can communicate the version through request headers, or query parameters. Completely different hostnames can even be used for major redesigns.
There are plenty of arguments about which approach is best, and that discussion can become an entire post by itself.
For now, I think there is a more important lesson.
How you version your API is significantly less important than the understanding of why the new API version exists.
If you are creating V2 because the implementation changed, you are probably thinking about the wrong version.
If you are creating V2 because the contract must change in a way that existing clients cannot understand, now we have a reason to talk about API versioning.
Eventually V1 needs to go away. Don't just kill it, and allow your clients start getting errors in production. They didn't do anything wrong. Retiring a contract has to be a coordinated change when your client depends on said contract.
A reasonable lifecycle might look something like this:
Release V2
Run V1 and V2 Simultaneously
Notify V1 clients
Publish migration documentation
Provide a migration window
Deprecate V1
Retire V1
Clients should know what is happening and when.
That can include release notes, documentation updates, developer portals, emails, and direct communication with teams that you know consume the API. The management around this can be its own topic as well, we're just talking about contracts.
You can also communicate deprecation through HTTP itself.
The Deprecation response header is now standardized for telling clients that a resource has been deprecated or will be deprecated.
The Sunset response header can communicate when a URI is expected to stop responding.
For example:
Deprecation: @1798761600
Sunset: Wed, 30 Jun 2027 23:59:59 GMT
Link: <https://api.example.com/docs/migration>; rel="deprecation"
A human reading an email can understand that V1 is going away.
A client, monitoring system, or development tool can potentially understand these headers too.
Retirement should not be a surprise. Give clients time to move. Run versions together when appropriate. Know who is using the version you are trying to retire. Then actually retire it.
Keeping twelve versions alive forever is not API lifecycle management. It is API archaeology. This is technical debt, and every version you keep around gives you another thing to build, test, document, monitor, and eventually fix.
There are a lot of details involved in API versioning causing us to say "it depends". Have the conversations. We can argue about URLs versus headers, or version numbering strategies.
We can build API gateways, developer portals, governance processes, and migration policies.
Those things help and are important.
If you can take one idea, one key concept, one statement away that explains it all from this post -Your API version is not the version of your application. It is the version of the promise you made to your clients.
Your implementation belongs to you, and is abstracted away from the client. Your contract is shared. Treat it accordingly.
And before you rename name to playerName five minutes before deployment because "it looks cleaner," maybe check who is using it first.