Join JS Mastery Pro to apply what you learned today through real-world builds, weekly challenges, and a community of developers working toward the same goal.
If you’re just starting out in backend development, API versioning might not seem like a big deal. But the moment you start working with real-world systems, you quickly realize how important it actually is.
Let me show you a few examples.
If you’ve ever used the Stripe API, you might have noticed something like this:
https://api.stripe.com/v1/balanceThe v1 in the URL stands for version one. Behind the scenes, Stripe likely has multiple versions of the same endpoint, while developers experiment or release updates.
Other public APIs follow the same approach:
https://api.nasa.gov/neo/rest/v1/feed
https://api.spacexdata.com/v3NASA’s endpoint is on version 1, while SpaceX is providing version 3. This versioning is not just cosmetic, it’s critical for keeping apps that rely on these APIs stable while the APIs evolve.
Speaking of building robust APIs, if you're serious about mastering the entire backend ecosystem, from architecture to deployment, we are building something special. Join the waitlist for the Ultimate Backend Course here.
Why API Versioning Matters? Let’s take Stripe as an example…
Let’s take Stripe as an example. Their API handles over 500 million requests daily, coming from hundreds of thousands of developers and businesses.
Imagine a small change, renaming a field from balance to current_balance. For a single developer, that might seem harmless. But all the apps relying on the old field suddenly break. Dashboards, payment processors, internal reports, all expecting balance could fail. For a platform that handles money, even a tiny mismatch can create serious, real-world problems.
This is exactly why versioning exists. Instead of changing the existing API that everyone relies on, Stripe releases a new version of the endpoint.
In short, versioning lets you update and improve your API while ensuring that existing clients don’t suddenly break. That’s why you’ll see most public APIs using /v* in their endpoints, it’s about stability, trust, and avoiding chaos.
It’s not just about keeping your users happy. Versioning is also essential for your own system. As your application grows, changes are inevitable. Without versioning, updates can easily create unexpected bugs or failures for anyone consuming your API.
Now that you understand why versioning is so important, let’s move on to how to actually implement versioning in Express.
There are a few common strategies for API versioning, but the easiest one to start with, and the one you’ll see in most public APIs, is URL versioning.
You include the version directly in the route path, like /v1/users or /v2/users, similar to what you saw earlier in the Stripe and NASA APIs. This allows you to maintain multiple versions of the same endpoint side by side.
Here’s a simple example:
import express from 'express';
const app = express();
// v1 route: original user info
const v1Router = express.Router();
v1Router.get('/users', (req, res) => {
res.json({
users: [
{ id: 1, fullName: 'Alice Johnson' },
{ id: 2, fullName: 'Bob Smith' },
],
version: 'v1',
});
});
// v2 route: breaking change! fullName split into firstName and lastName
const v2Router = express.Router();
v2Router.get('/users', (req, res) => {
res.json({
users: [
{ id: 1, firstName: 'Alice', lastName: 'Johnson' },
{ id: 2, firstName: 'Bob', lastName: 'Smith' },
],
version: 'v2',
});
});
// Mount routers
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
app.listen(3000, () => console.log('Server running on port 3000'));Let’s break down what’s happening here:
This is exactly why versioning matters. Without it, a change like this could easily crash dashboards, background jobs, reports, or any logic that depends on fullName. Public APIs like Stripe or SpaceX follow this approach so they can improve their APIs while keeping existing integrations stable.
If you prefer cleaner URLs, you can also version your API using custom headers.
Here’s the same breaking change example using headers:
app.get('/users', (req, res) => {
const version = req.headers['x-api-version'];
// v2 response, breaking change
if (version === '2') {
return res.json({
users: [
{ id: 1, firstName: 'Alice', lastName: 'Johnson' },
{ id: 2, firstName: 'Bob', lastName: 'Smith' },
],
version: 'v2',
});
}
// v1 response, original contract
res.json({
users: [
{ id: 1, fullName: 'Alice Johnson' },
{ id: 2, fullName: 'Bob Smith' },
],
version: 'v1',
});
});Clients that want the new version simply send this header:
X-API-Version: 2Header versioning gives you flexibility, but it also requires solid documentation. If a client forgets to send the version header, they may unknowingly consume the wrong response format. That’s why many public APIs prefer URL versioning, it’s explicit, visible, and harder to misuse.
These examples are intentionally simple to help you understand the concept. In real projects, you should organize your versioned code properly instead of keeping everything in one file.
As your API grows, separating versions into folders makes your codebase much easier to maintain:
routes/
v1/
users.ts
products.ts
v2/
users.ts
products.tsThen in your main app.ts file:
import v2Usersfrom"./routes/v2/users";
app.use("/api/v1/users", v1Users);
app.use("/api/v2/users", v2Users);This approach keeps version-specific logic isolated, so your codebase doesn’t slowly turn into a mess as new versions are added.
This is still a basic setup, but it gives you a solid foundation. Once you understand this structure, scaling it is much easier. If you want to go deeper into organizing Express projects at scale, you can check this out: Link
With this structure in place, you can confidently:
And if you’re building a public API, versioning alone is not enough. Because versioning lets you ship changes safely, but it doesn’t solve everything on its own. At some point, old versions need to go away. That’s where deprecation exists.
Let’s take a look at how to do that.
Managing the full lifsecycle of an API, from versioning to deprecation, is exactly what separates junior developers from seniors. If you want to master these advanced workflows, join the waitlist for the Ultimate Backend Course.
Deprecation is not about suddenly breaking APIs. It’s about communication and transition. You’re telling users:
“This version will stop working in the future, here’s how and when to move forward.”
The worst thing you can do is silently remove or change an old version. Clients might still be using it in production, and they may not even notice until things start failing.
Instead, keep the old version running and clearly mark it as deprecated.
One simple way to do this is by sending a response header:
app.use('/api/v1', (req, res, next) => {
res.setHeader('X-API-Deprecated', 'true');
res.setHeader('X-API-Deprecation-Message', 'v1 will be removed on 2025-06-01. Please upgrade to v2.');
next();
});Now every client using v1 gets a clear signal that they need to upgrade, without breaking anything.
Deprecation only works if users know what to do next. Always make it clear:
For example, in our earlier case:
This kind of information should live in your documentation, changelog, or even in the response message itself.
Before removing a version, you should know whether people are still using it. Track how many requests are hitting old versions:
This helps you decide when it’s actually safe to shut down a version.
Once most users have migrated:
This gradual approach builds trust. Users know your API won’t suddenly break without notice.
Versioning lets you change your API safely. Deprecation ensures users can move forward without panic. Together, they allow you to evolve your API, ship improvements faster, and still keep existing clients happy.
That’s the same approach you see in mature public APIs like Stripe, GitHub, or AWS and it’s exactly what you should aim for in your own Express APIs.
Versioning your API is great, but there are a few extra things you can do to make it rock-solid and easy to maintain.
Following these simple practices will help you maintain a stable, predictable API while still moving fast and improving your endpoints over time.