Skip to content

Express API Versioning

As an API evolves, breaking changes are sometimes unavoidable. Versioning lets you introduce them without breaking existing clients. This lesson covers the most common Express versioning strategies.

Why APIs Need Versioning

Once external clients depend on your API's shape, changing a field name or response structure can break their integrations. Versioning gives you a safe way to introduce breaking changes behind a new version identifier while keeping the old version running unchanged.

The most common and easiest-to-understand approach in Express is URL path versioning, mounting an entirely separate router (or router tree) for each version prefix.

const v1Router = require('./routes/v1');
const v2Router = require('./routes/v2');

app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

Both versions can run simultaneously, existing clients keep calling /api/v1/... while new clients adopt /api/v2/... at their own pace.

Versioning Strategy Syntax

// URL path versioning
app.use('/api/v1', v1Router);

// Header-based versioning
app.use('/api', (req, res, next) => {
  req.apiVersion = req.headers['x-api-version'] || '1';
  next();
});
  • URL path versioning (/api/v1/...) is the simplest to implement and the easiest for clients to reason about.
  • Header-based versioning keeps URLs stable but requires custom middleware to branch on a header value.
  • Whichever strategy you choose, keep each version's router tree as self-contained as possible.
  • Deprecate old versions loudly (docs, response headers, changelogs) well before removing them.

API Versioning Cheatsheet

Trade-offs between the common versioning strategies.

Strategy Example Trade-off
URL path /api/v1/users Simple, visible, but duplicates routers per version
Query param /api/users?version=2 Easy to add, easy for clients to forget
Custom header X-API-Version: 2 Clean URLs, but less discoverable/testable in a browser
Accept header Accept: application/vnd.myapi.v2+json Standards-friendly, more complex to implement

Structuring Versioned Routers

A practical folder layout keeps each version's routes, and only the controllers that actually changed, separate, while sharing unaffected code between versions.

routes/
├── v1/
│   ├── users.router.js
│   └── index.js
└── v2/
    ├── users.router.js  // only rewritten because the shape changed
    └── index.js

// app.js
app.use('/api/v1', require('./routes/v1'));
app.use('/api/v2', require('./routes/v2'));

Deprecating an Old Version

Instead of removing an old version abruptly, add a deprecation warning header so well-behaved clients can detect and react to the upcoming removal.

app.use('/api/v1', (req, res, next) => {
  res.set('Deprecation', 'true');
  res.set('Sunset', 'Wed, 31 Dec 2026 23:59:59 GMT');
  next();
}, v1Router);

Common Mistakes

  • Introducing breaking changes to an existing version instead of releasing a new one.
  • Versioning the entire API when only one endpoint actually changed shape.
  • Removing an old version without any deprecation notice or transition period.
  • Duplicating an entire version's codebase when only a handful of routes actually differ.

Key Takeaways

  • Versioning protects existing clients from breaking changes as an API evolves.
  • URL path versioning (/api/v1) is the simplest and most common strategy in Express apps.
  • Keep each version's router tree self-contained, but share unaffected code where possible.
  • Communicate deprecation clearly before removing an old version entirely.

Pro Tip

Only version the specific resource that changed shape when possible, run /api/v1/orders alongside a new /api/v2/orders while every other /api/v1/... route keeps serving both versions unchanged.