API Versioning
Once clients depend on your API, you can't freely change it — a renamed field or removed endpoint breaks their code. Versioning lets you evolve the API while keeping old clients working: you ship v2 with the changes and leave v1 running until everyone migrates. This page covers the versioning strategies, which changes actually require a new version, and how to deprecate gracefully.
Breaking vs non-breaking changes
Change | Breaking? |
|---|---|
Adding a new endpoint | No |
Adding an optional field to a response | No (usually) |
Adding an optional request param | No |
Removing or renaming a field | Yes |
Changing a field's type or meaning | Yes |
Making an optional param required | Yes |
Changing status codes or error shapes | Yes |
Strategy 1: URL path versioning (most common)
import { Router } from 'express'
const v1 = Router()
v1.get('/users', listUsersV1)
const v2 = Router()
v2.get('/users', listUsersV2) // new shape, new behavior
app.use('/v1', v1)
app.use('/v2', v2)
// GET /v1/users → old contract
// GET /v2/users → new contractStrategy 2: header / media-type versioning
// Client picks a version via a header:
// Accept: application/vnd.myapi.v2+json
app.use((req, res, next) => {
const accept = req.get('Accept') || ''
req.apiVersion = accept.includes('v2') ? 2 : 1
next()
})
app.get('/users', (req, res) => {
return req.apiVersion === 2 ? listUsersV2(req, res) : listUsersV1(req, res)
})Other strategies (use sparingly)
Strategy | Example | Note |
|---|---|---|
Query param |
| Easy but pollutes every URL |
Custom header |
| Non-standard but simple |
Subdomain |
| Heavy; needs DNS/infra work |
Deprecation — retire versions gracefully
// Warn callers of v1 that it's going away, with a sunset date:
v1.use((req, res, next) => {
res.set('Deprecation', 'true')
res.set('Sunset', 'Wed, 31 Dec 2026 23:59:59 GMT') // RFC 8594
res.set('Link', '</v2/docs>; rel="successor-version"')
next()
})Keep version differences thin
// Share the core logic; let each version own only its formatting:
async function getUsers(query) {
return db.users.find(query) // shared business logic
}
const listUsersV1 = async (req, res) =>
res.json((await getUsers(req.query)).map(toV1Shape))
const listUsersV2 = async (req, res) =>
res.json({ data: (await getUsers(req.query)).map(toV2Shape) })