API Versioning: URL vs Header vs Query Parameter
You shipped v1. Now v2 needs breaking changes. How do you support both without breaking existing clients?
Three Approaches
| Method | Example | Pros | Cons |
|---|---|---|---|
| URL path | /api/v1/users |
Simple, cacheable | URL proliferation |
| Header | Accept: application/vnd.api.v2+json |
Clean URLs | Harder to test |
| Query | /api/users?version=2 |
Easy to switch | Easy to forget |
URL Path (Recommended)
import { Router } from "express";
const v1 = Router();
v1.get("/users", (req, res) => {
res.json(users.map(u => ({ id: u.id, name: u.name, email: u.email })));
});
const v2 = Router();
v2.get("/users", (req, res) => {
res.json({
data: users,
meta: { page, perPage, total },
});
});
app.use("/api/v1", v1);
app.use("/api/v2", v2);
Sharing Logic Between Versions
Do not duplicate business logic. Version the response shape, not the service layer:
async function getUsers(page: number, limit: number) {
return db.query("SELECT * FROM users LIMIT $1 OFFSET $2", [limit, (page - 1) * limit]);
}
// v1
v1.get("/users", async (req, res) => {
const users = await getUsers(1, 50);
res.json(users);
});
// v2
v2.get("/users", async (req, res) => {
const page = Number(req.query.page) || 1;
const users = await getUsers(page, 20);
res.json({ data: users, meta: { page } });
});
Sunsetting Old Versions
- Announce deprecation with
Deprecationheader - Log v1 usage to track adoption
- Give 6-12 months migration window
- Return 410 Gone after sunset date
v1.use((req, res, next) => {
res.set("Deprecation", "true");
res.set("Sunset", "Sat, 01 Jun 2025 00:00:00 GMT");
next();
});
Part of my Production Backend Patterns series. Follow for more practical backend engineering.
Top comments (0)