DEV Community

Young Gao
Young Gao

Posted on

API Versioning: URL vs Header vs Query Parameter

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);
Enter fullscreen mode Exit fullscreen mode

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 } });
});
Enter fullscreen mode Exit fullscreen mode

Sunsetting Old Versions

  1. Announce deprecation with Deprecation header
  2. Log v1 usage to track adoption
  3. Give 6-12 months migration window
  4. 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();
});
Enter fullscreen mode Exit fullscreen mode

Part of my Production Backend Patterns series. Follow for more practical backend engineering.

Top comments (0)