You can version your APIs by prefixing your URLs with the version number, or by setting a custom header.
Here’s a quick example of how to set this up in Express.js:
const express = require('express');
const app = express();
// Version 1 of the API
const apiV1 = express.Router();
apiV1.get('/users', (req, res) => {
res.send('Hello from API v1 users!');
});
app.use('/v1', apiV1);
// Version 2 of the API
const apiV2 = express.Router();
apiV2.get('/users', (req, res) => {
res.send('Hello from API v2 users!');
});
app.use('/v2', apiV2);
// API versioning via headers
app.get('/users', (req, res) => {
const apiVersion = req.headers['x-api-version'];
if (apiVersion === '1') {
res.send('Hello from API v1 users via header!');
} else if (apiVersion === '2') {
res.send('Hello from API v2 users via header!');
} else {
res.status(400).send('API version not specified or unsupported.');
}
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
If you run this server and send requests:
GET http://localhost:3000/v1/userswill returnHello from API v1 users!GET http://localhost:3000/v2/userswill returnHello from API v2 users!GET http://localhost:3000/userswith headerX-API-Version: 1will returnHello from API v1 users via header!GET http://localhost:3000/userswith headerX-API-Version: 2will returnHello from API v2 users via header!
This allows you to introduce breaking changes without affecting existing clients. When you need to make a change that would break existing functionality, you create a new version of your API. Existing clients can continue to use the old version while new clients can adopt the new one.
The core idea is to have different code paths for different API versions. In the Express example above, we’re using express.Router() to create modular route handlers. app.use('/v1', apiV1) mounts the apiV1 router at the /v1 path, meaning any request starting with /v1 will be handled by that router. Similarly, /v2 directs to apiV2. For header-based versioning, a single /users route handler checks the x-api-version header and branches its logic accordingly.
This separation is crucial. It means that changes made to apiV2 don’t accidentally affect apiV1. You can deploy a new version of your API, and as long as clients are still pointing to the old URL prefix (e.g., /v1), their applications won’t break. They can migrate to /v2 at their own pace.
The choice between URL prefixes and headers often comes down to preference and how you want your API to be consumed. URL prefixes are generally more discoverable and easier to test directly in a browser. Header-based versioning keeps the resource URLs cleaner, which can be appealing for RESTful purists. Both methods achieve the same goal: allowing concurrent versions of your API to exist.
When you’re managing multiple versions, it’s important to have a clear deprecation strategy. You’ll eventually want to retire older versions. This involves communicating to your users when a version will be sunset, providing ample notice, and then eventually shutting down the old endpoints. A common practice is to support older versions for at least 6-12 months after announcing deprecation.
The most surprising thing about versioning is how easily it can become a leaky abstraction if not managed carefully. Developers might start coupling logic between versions, or worse, introduce new features into an older, stable version thinking it’s a minor change, thereby breaking the isolation that versioning is meant to provide. Each version should ideally be treated as a completely independent entity, with its own lifecycle and maintenance.
A subtle but powerful technique is to use middleware to automatically select the correct API version based on the request, rather than having it explicitly in every route handler. This can make your route definitions cleaner and centralize version logic. For example, you could have a middleware that inspects the Accept header (e.g., application/vnd.myapi.v1+json) or a custom header, and then attaches the appropriate versioned router or handler to the request object before it reaches your specific route logic.
As your API grows and you introduce more versions, you’ll eventually need to consider how to handle requests that don’t specify a version at all.