In this lesson, you will learn how to support multiple concurrent API versions by routing incoming requests based on custom headers.
As your application grows, you will inevitably need to introduce breaking changes—like renaming a field or changing a data structure—without breaking existing mobile apps or third-party integrations that rely on your current API. Instead of forcing everyone to upgrade simultaneously, we use Versioned API Architecture to serve different logic based on a version identifier.
While some developers put versioning in the URL (e.g., /api/v1/users), using an Accept or a custom X-API-Version header is a cleaner approach that keeps your URLs resource-oriented.
Implementation via Middleware
We can implement this by creating a lightweight piece of middleware that inspects the request headers and attaches the requested version to the req object for later use in our route handlers.
const apiVersioning = (req, res, next) => {
// Default to version 1 if no header is provided
const version = req.get('X-API-Version') || '1';
req.apiVersion = version;
next();
};
// Usage in an Express app
app.use(apiVersioning);
app.get('/users', (req, res) => {
if (req.apiVersion === '2') {
return res.json({ status: 'success', data: [...] }); // New structure
}
return res.json([...]); // Legacy structure
});
By assigning the version to req.apiVersion early in the middleware stack, your route handlers can conditionally execute logic or select different service layers based on the client's needs. This allows you to maintain "Legacy" and "Current" logic within the same codebase side-by-side.
As your API matures, you can eventually deprecate older versions by checking the req.apiVersion and returning a 410 Gone status code or a warning header, allowing you to clean up technical debt without impacting active clients. This approach keeps your infrastructure flexible and your client integrations stable.
Key Takeaway: Versioning via request headers allows you to decouple your API's evolution from the client's upgrade cycle, ensuring backward compatibility while enabling continuous feature delivery.