API Versioning: Evolving Your API Without Breaking Old Clients
We cover how not to break old clients while evolving the API over time: what a breaking change is, why versioning is needed, the URL/header/query methods, and setting up versioning in .NET.
From the moment an API goes live, it's no longer just yours, but also belongs to those who use it. Mobile apps, other services, third-party integrations connect to it. And one day you'll need to change that API; you add a new field, you fix the structure of a response. And this is exactly where the danger begins: the change you make can suddenly break dozens of applications depending on you. Today we talk about the approach that solves this problem: API versioning.
What Is a Breaking Change?
Not every change is dangerous. Some changes are backward compatible, that is, they don't affect old clients. For example, adding a new field to a response is usually safe; the old application ignores that new field and continues its work. But some changes are breaking: changing a field's name, removing a field, restructuring a response from scratch. These suddenly make old code, written trusting that field, stop working.
Think of it like an apartment building's mailboxes. If you add a new one, no one is affected. But if you change the numbers of the existing boxes, everyone sending mail to that address has their mail go to the wrong place. A breaking change in an API is exactly this: a small fix for you, a complete disaster for someone depending on you.
So Why Don't We Just Keep the Old Version?
The first solution that comes to mind might be: "let me never make a breaking change, let me always keep the API backward compatible." As nice as it sounds, this becomes impossible after a point. Applications evolve, requirements change, and sometimes it turns out that old design decisions were wrong. Freezing the API forever makes it unable to evolve. This is exactly the deadlock versioning offers a solution to: a way to publish the new without breaking the old.
The Basic Idea of Versioning
The logic of versioning is simple: you keep multiple versions of the same API alive at the same time. Old clients keep using v1, new clients move to v2. You make whatever breaking change you want in v2, but since you don't touch v1, the old applications notice nothing. Everyone works peacefully in their own version. Clients move to the new version in their own time, when they're ready; no one is moved by force.
Where Should We Put the Version? Three Methods
There are a few ways for a request to specify which version it wants. Three are common, and each has its own advantage.
In the URL
The most common and most visible method. The version is placed directly in the address:
/api/v1/users
/api/v2/users
The biggest advantage of this method is clarity. Anyone looking at the address instantly sees which version they're using; testing, documenting, and trying it from the browser is very easy. Because of this clarity, it's the most frequently preferred method in practice.
In a Header
The version is carried not in the address but in a header of the request. For example, with a custom X-Api-Version: 2 header. In this approach the address stays clean, independent of the version. But its visibility is low; to understand which version is being used, you have to look at the request's headers, looking at the address isn't enough.
In the Query String
The version is added to the end of the address as a parameter: /api/users?api-version=2. It's easy to use but doesn't look as clean as the URL method; the version information can get lost among other query values like filtering parameters.
Setting Up Versioning in .NET
.NET solves versioning through a package. First you add the package:
dotnet add package Asp.Versioning.Mvc
Then you register the versioning service in Program.cs:
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
});
This setting says three things. Let the default version be 1.0; if the client doesn't specify a version, let the default be used automatically; and let the information about which versions are available be returned in the responses. This last setting makes it easier for those using your API to discover which versions exist.
Then you mark your controllers to versions. Assuming we chose the URL-based method, it looks like this:
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/users")]
public class UsersV1Controller : ControllerBase
{
[HttpGet]
public IActionResult GetAll()
{
return Ok("v1 user list");
}
}
[ApiVersion("1.0")] specifies which version this controller belongs to, while the v{version:apiVersion} in [Route(...)] specifies where in the address the version is placed. When you release a new version, you write a separate controller marked with [ApiVersion("2.0")] and don't touch the old one at all. The two versions live side by side, without affecting each other.
When Should You Release a New Version?
Setting up versioning is easy, but the real matter is knowing when to use it. Simple rule: release a new version only when you need to make a breaking change. For backward-compatible changes like adding a field, there's no need for a new version; you can do it in the current version. But if you're removing a field, changing its name, or restructuring the response from scratch, that's when it's time for v2. Multiplying versions unnecessarily also increases the maintenance burden; each new version means one more contract you have to keep alive.
A Small Experiment
Add the versioning package to your project and create two controllers: one marked to v1, the other to v2, but managing the same resource (for example, users). Deliberately structure the response differently in v2. Then go to both /api/v1/users and /api/v2/users from the browser and see the two different responses. Observing that both work at the same time, without affecting each other, clearly shows why versioning is so relieving.
In the next article we move on to the security and identity block. The first topic will be separating two concepts most developers confuse: the difference between authentication and authorization. The time is coming to control who can use the API whose versions you kept alive today, and how.