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;
const userRepo = require('../data/userRepo');
function serialize(user, version) {
if (version === 'v1') {
return { id: user.id, name: `${user.firstName} ${user.lastName}`, email: user.email };
}
return {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
createdAt: user.createdAt
};
}
async function listUsers(req, res, next) {
try {
const users = await userRepo.findAll({ limit: Number(req.query.limit) || 50 });
res.json({ data: users.map((u) => serialize(u, req.apiVersion)) });
} catch (err) {
next(err);
}
}
async function createUser(req, res, next) {
try {
const user = await userRepo.create(req.body);
res.status(201).json({ data: serialize(user, req.apiVersion) });
} catch (err) {
next(err);
}
}
// v2-only aggregate endpoint
async function getUserStats(req, res, next) {
try {
const stats = await userRepo.aggregateStats();
res.json({ data: stats });
} catch (err) {
next(err);
}
}
module.exports = { listUsers, createUser, getUserStats, serialize };
const express = require('express');
const controller = require('../controllers/userController');
const router = express.Router();
router.use((req, res, next) => {
req.apiVersion = 'v1';
next();
});
function deprecationNotice(req, res, next) {
res.set('Deprecation', 'true');
res.set('Sunset', 'Wed, 31 Dec 2025 23:59:59 GMT');
res.set('Link', '</api/v2>; rel="successor-version"');
next();
}
router.use(deprecationNotice);
router.get('/users', controller.listUsers);
router.post('/users', controller.createUser);
module.exports = router;
const express = require('express');
const v1Router = require('./routes/v1Router');
const v2Router = require('./routes/v2Router');
const app = express();
app.use(express.json());
const DEFAULT_VERSION = 'v2';
const SUPPORTED = ['v1', 'v2'];
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// Bare /api resolves to the current stable version
app.use('/api', (req, res) => {
res.redirect(307, `/api/${DEFAULT_VERSION}${req.path === '/' ? '' : req.path}`);
});
// Any /api/<unknown> falls through to a clear error
app.use('/api/:version', (req, res) => {
res.status(404).json({
error: 'unsupported_api_version',
requested: req.params.version,
supported: SUPPORTED
});
});
app.use((err, req, res, next) => {
res.status(err.status || 500).json({ error: err.message });
});
module.exports = app;
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
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
Django URL namespacing and reverse lookups
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
static values = { delay: { type: Number, default: 250 } }
Debounced live search with Stimulus + Turbo Streams
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
package deps
import (
"net"
"net/http"
"time"
HTTP client tuned for production: timeouts, transport, and connection reuse
Share this code
Here's the card — post it anywhere.