Let me be direct.
Most developers don’t think about versioning until something breaks in production. An endpoint changes, a mobile app stops working, a client complains, and suddenly everyone is scrambling.
Versioning is not damage control. It’s architecture.
If you truly understand versioning and its application, you build robust APIs that evolve safely.
Let’s break it down properly, starting with what versioning really is.
1. What is Versioning (Really)?
At a surface level, versioning means assigning versions such as v1, v2, and v3 to your API.
But that definition is incomplete. Versioning is about managing change over time without breaking existing consumers. Think about this:
You have an endpoint:
GET /users
It returns:
{
"id": 1,
"name": "alif"
}
Now you decide to split the name into firstName and lastName.
{
"id": 1,
"firstName": "alif",
"lastName": "xyz"
}
Looks harmless.
But any client expecting a name will now break.
That’s where versioning comes in. Instead of changing the existing contract, you create a new one:
GET /v2/users
Old clients continue using v1. New clients move to v2. Nothing breaks. That’s versioning.
2. Why Versioning Matters
Here’s the uncomfortable truth:
Your API is not just code. It’s a contract.
And contracts cannot change arbitrarily.
Without versioning:
- You deploy a change → clients break.
- Third-party integrations fail silently.
- Debugging becomes a nightmare.
With versioning:
- You can iterate safely.
- Clients upgrade on their own timeline.
- You maintain backward compatibility.
- You reduce production risk significantly.
A simple way to think about it:
Versioning is insurance for your API.
You might not need it today. But when you do, you really do.
3. Types of API Versioning
There isn’t just one way to version an API. There are multiple strategies, each with trade-offs.
Let’s go through the important ones.
3.1 URI Versioning (Most Common)
This is the one you’ve definitely seen:
/v1/users
/v2/users
Most production systems use this because it’s predictable and practical.
3.2 Header Versioning
2nd approach is to pass the version in the headers:
GET /users
Headers:
Accept: application/vnd.myapp.v1+json
or
x-api-version: 1
3.3 Query Parameter Versioning
GET /users?version=1
Why use it:
- Easy to implement
Downside:
- Feels hacky
- Not ideal for long-term API design
3.4 Media Type Versioning
It is used in more strict REST systems.
Accept: application/vnd.myapp.v2+json
It’s such a powerful approach, but overkill for most applications.
So which one should you use?
If you want a practical answer:
Use URI versioning unless you have a strong reason not to.
It’s the most maintainable for teams and the easiest to communicate.
4. Versioning Strategy (This Is Where Most People Fail)
Versioning is not just about adding v1.
It’s about when and why you create a new version.
Here’s the rule most experienced teams follow:
Create a new version when:
- You remove a field
- You rename a field
- You change the response structure.
- You change the business logic behavior.
Don’t version for:
- Adding optional fields
- Adding new endpoints
- Internal refactoring
If your change is backward compatible, don’t version. If it’s breaking, version it.
That’s the line.
5. How Versioning Works in NestJS
Now let’s make this practical. NestJS has built-in support for versioning, which is something many frameworks don’t handle cleanly. You don’t need hacks. You don’t need custom middleware. You just enable it.
Step 1: Enable Versioning
In your main.ts:
import { VersioningType } from '@nestjs/common';
app.enableVersioning({
type: VersioningType.URI,
});
That’s it. Versioning is now active.
Step 2: Define Versions in Controllers
import { Controller, Get, Version } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get()
@Version('1')
getUsersV1() {
return [{ id: 1, name: 'alif' }];
}
@Get()
@Version('2')
getUsersV2() {
return [{ id: 1, firstName: 'alif', lastName: 'xyz' }];
}
}
Now:
GET /v1/users → getUsersV1
GET /v2/users → getUsersV2
Same route. Different versions.
Step 3: Default Version
You can optionally define a default version:
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});
So if someone hits /users, it resolves to v1.
Step 4: Header Versioning
Alternatively, you can define versions in the header:
app.enableVersioning({
type: VersioningType.HEADER,
header: 'x-api-version',
});
Request:
GET /users
x-api-version: 2
6. Real-World Pattern
Here’s how mature systems handle versioning:
- Keep v1 stable
- Introduce v2 for breaking changes.
- Deprecate old versions gradually.
- Never delete immediately
You’ll often see:
/v1 → legacy clients
/v2 → current production
/v3 → in development
Versioning becomes part of your release strategy, not an afterthought.
Internally, each of these versions is evolving like this:
v1.0.0 → v1.0.1 → v1.1.0 → v1.2.3
That’s MAJOR.MINOR.PATCH at work. This is called semantic Versioning.
Conclusion:
Most developers treat versioning as a technical detail, while it’s a design decision about how your system evolves. If you ignore it, you’ll ship faster in the short term and pay for it later. If you understand it early, you build systems that don’t collapse under change. And in backend engineering, that’s the difference between code that works and systems that last.
Comments
Loading comments…