javascript 117 lines · 4 tabs

Versioning an Express API with Router-per-Version Mounts and a Shared Controller Module

Shared by codesnips Aug 2026
4 tabs
const express = require('express');
const controller = require('../controllers/userController');

const router = express.Router();

router.use((req, res, next) => {
  req.apiVersion = 'v2';
  next();
});

// Reused unchanged from the shared controller
router.get('/users', controller.listUsers);
router.post('/users', controller.createUser);

// New surface area that only exists in v2
router.get('/users/stats', controller.getUserStats);

module.exports = router;
4 files · javascript Explain with highlit

This snippet demonstrates a pragmatic approach to versioning an HTTP API in Express: mounting one Router per version under a version prefix while keeping the underlying business logic in a single shared controller module. The core idea is that versioning is a routing and shaping concern, not a reason to fork the entire codebase. Most endpoints behave identically across versions, so duplicating handlers invites drift and bugs; instead each version's router reuses the same controller functions and only overrides what actually changed.

In userController.js, the controller exposes plain async handlers like listUsers and createUser that talk to the data layer and pass results to a serializer. The important seam is serialize, which accepts a version argument so response shaping can differ per version without branching the query logic. Version v1 returns a flat name field for backward compatibility, while v2 splits it into firstName/lastName and adds createdAt. Handlers read req.apiVersion rather than hardcoding, so the same function serves any mount point.

In v1Router.js and v2Router.js, each router sets req.apiVersion in a small middleware, then wires routes to the shared controller. v1Router adds a Deprecation header via deprecationNotice so clients get a machine-readable signal to migrate, a real-world nicety mandated by RFC-style sunset practices. v2Router reuses listUsers and createUser unchanged but introduces a genuinely new endpoint, getUserStats, that only exists in v2 — showing how a version can add surface area without touching older code.

In app.js, the versions are mounted with app.use('/api/v1', ...) and app.use('/api/v2', ...). A default-version redirect maps bare /api calls to the current stable version, and an unknown-version handler returns a clear 404 listing supported versions. The trade-off is that shared controllers must stay version-aware where shaping diverges; the payoff is that fixing a query bug fixes it everywhere at once. This pattern fits APIs where most logic is stable and only response contracts or a few endpoints evolve between releases.


Related snips

Share this code

Here's the card — post it anywhere.

Versioning an Express API with Router-per-Version Mounts and a Shared Controller Module — share card
Link copied