Skip to content

API Versioning in Node.js

APIs change over time, but existing clients often can't update instantly. This lesson covers strategies for versioning a Node.js API so you can evolve it safely without breaking consumers already depending on it.

Why Version an API?

Once an API has external consumers, mobile apps, third-party integrations, other teams' services, changing a response shape or removing a field can silently break them. Versioning gives you a way to introduce breaking changes in a new version while keeping the old version working exactly as before, until consumers are ready to migrate.

The two most common approaches are URL-based versioning (/api/v1/users, /api/v2/users) and header-based versioning (a custom header like Accept-Version: 2), each with different tradeoffs in discoverability and implementation complexity.

import express from 'express';
import v1Router from './routes/v1/index.js';
import v2Router from './routes/v2/index.js';

const app = express();

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

URL-based versioning is the most explicit and discoverable approach, a client can see exactly which version it's calling just by looking at the URL.

Versioning Strategies Compared

URL-based:     GET /api/v1/users
Header-based:  GET /api/users  (Accept-Version: 1)
Query-based:   GET /api/users?version=1
  • URL-based versioning is the simplest to implement and the easiest for clients to understand.
  • Header-based versioning keeps URLs stable but requires clients to set a header correctly.
  • Query-based versioning is uncommon and can conflict with other filtering query parameters.
  • Whichever approach you choose, apply it consistently across the entire API.

API Versioning Cheatsheet

Tradeoffs between the common versioning approaches.

Strategy Pros Cons
URL-based (/v1/...) Explicit, cacheable, easy to route URLs change between versions
Header-based Stable URLs Less discoverable, easy to forget
Query-based Simple to add Can clash with other query params
No versioning Simplest short-term Any change risks breaking clients

Structuring Versioned Routes in Code

Organizing route files by version (routes/v1/, routes/v2/) keeps each version's logic isolated, so changes to v2 can't accidentally affect v1 clients still depending on the old behavior.

routes/
  v1/
    users.js
    orders.js
  v2/
    users.js   // updated response shape
    orders.js  // unchanged, can re-export from v1

Unchanged resources in a new version can simply re-export the previous version's router, avoiding duplicated code for parts of the API that didn't actually change.

Deprecating Old Versions Gracefully

Versioning isn't just about adding new versions, it also means eventually retiring old ones. A Deprecation header (or documented sunset date) gives consumers advance warning before an old version stops being supported.

  • Announce deprecation well ahead of removing an old version.
  • Add a Deprecation/Sunset response header to deprecated endpoints.
  • Monitor usage of old versions to know when it's actually safe to remove them.

Common Mistakes

  • Making breaking changes to an existing API version instead of introducing a new version.
  • Choosing a versioning strategy inconsistently across different parts of the same API.
  • Removing an old API version without warning or a deprecation period.
  • Duplicating an entire version's codebase instead of re-exporting unchanged resources between versions.

Key Takeaways

  • Versioning lets you introduce breaking changes without disrupting existing API consumers.
  • URL-based versioning is the most common and most discoverable strategy.
  • Organizing routes by version in code keeps changes isolated between versions.
  • Deprecate old versions with clear warnings before removing them entirely.

Pro Tip

Decide on your versioning strategy before your first external consumer integrates with the API, retrofitting versioning onto an unversioned API already in production use is far more disruptive than starting with /api/v1/ from day one.