Ship Breaking Changes Without Breaking Clients
API Versioning
Versioning lets you evolve an API while keeping older clients alive. URL path is the pragmatic choice.
What you'll learn
- Pick a versioning style
- Mount versioned routers
- Plan deprecations
You will break your API. Versioning lets you do it without breaking existing clients.
URL Path — the Pragmatic Choice
import v1 from "./routes/v1/index.js";
import v2 from "./routes/v2/index.js";
app.use("/api/v1", v1);
app.use("/api/v2", v2); Pros: visible in URLs, simple to test, copy-paste a curl command and the version is right there.
Cons: a touch unREST-ful (a resource has multiple URLs).
Header — the Pure Approach
Accept: application/vnd.myapi.v2+json Cleaner in theory. In practice — debugging curl with custom Accept headers gets annoying fast, and intermediaries (CDNs, proxies) may strip them.
What Counts As Breaking
| Change | Breaking? |
|---|---|
| Add a new endpoint | No |
| Add a new optional field to a response | No |
| Add a new required field to a request | Yes |
| Rename a response field | Yes |
| Change a field’s type | Yes |
| Tighten a validation rule | Yes |
| Remove an endpoint | Yes |
| Change a status code | Yes |
When you’re about to make a Yes — version.
Versioned Code
Two ways:
Separate Files
src/routes/v1/users.js
src/routes/v2/users.js Simple but doubles code. Fine for small APIs.
Shared Controllers, Versioned Adapters
// shared service
const user = await userService.find(id);
// v1 response
res.json({ user });
// v2 response (renamed wrapper, added field)
res.json({ data: { ...user, displayName: user.name } }); The service stays the same. Only the controller’s response shape varies by version.
Deprecation Plan
Don’t accumulate versions forever. The lifecycle:
- Ship v2 — announce the new version
- Keep v1 running — typically 6-12 months
- Send
Deprecationheaders on v1 responses
v1Router.use((req, res, next) => {
res.set("deprecation", "true");
res.set("sunset", "Thu, 31 Dec 2026 23:59:59 GMT");
res.set("link", '</api/v2>; rel="successor-version"');
next();
}); - Monitor — what’s still hitting v1?
- Sunset — turn off v1 once usage is near zero
The Easy Way Out
Best version of any API: one that never needs versioning. Make fields additive, never required-once-optional. Use enums + extras (“we may add new values”). Stay loose.
Error Responses →