API Versioning

URL Prefix and Header Versioning with Router Prefixes

API Versioning

Add version identifiers to a Koa REST API using URL path prefixes and Accept-Version headers, and mount multiple router versions side-by-side.

3 min read Level 2/5 #koa#api-versioning#router
What you'll learn
  • Mount versioned routes under a /v1 and /v2 URL prefix using @koa/router
  • Implement header-based versioning by reading a custom request header
  • Explain the trade-offs between URL and header versioning strategies

An API version strategy lets you ship breaking changes without immediately breaking existing clients. The two most common approaches are URL prefix versioning (/v1/articles) and header versioning (API-Version: 2).

URL Prefix Versioning

Create a separate Router instance per version and set a prefix:

// src/routes/v1/articles.js
import Router from "@koa/router";
import * as v1 from "../../controllers/v1/articles.js";

const router = new Router({ prefix: "/v1/articles" });
router.get("/",    v1.listArticles);
router.post("/",   v1.createArticle);
router.get("/:id", v1.getArticle);

export default router;
// src/routes/v2/articles.js
import Router from "@koa/router";
import * as v2 from "../../controllers/v2/articles.js";

const router = new Router({ prefix: "/v2/articles" });
router.get("/",    v2.listArticles);
router.post("/",   v2.createArticle);
router.get("/:id", v2.getArticle);

export default router;

Mount both in app.js:

import Koa from "koa";
import bodyParser from "koa-bodyparser";
import v1Articles from "./routes/v1/articles.js";
import v2Articles from "./routes/v2/articles.js";

const app = new Koa();
app.use(bodyParser());
app.use(v1Articles.routes());
app.use(v1Articles.allowedMethods());
app.use(v2Articles.routes());
app.use(v2Articles.allowedMethods());

app.listen(3000);

URL prefix versioning is explicit and easy to document — clients can see the version directly in the URL.

Header Versioning

Header versioning keeps URLs clean but requires clients to set a custom header such as API-Version: 2. A middleware reads the header and dispatches to the correct handler:

// src/middleware/versionRouter.js
import * as v1 from "../controllers/v1/articles.js";
import * as v2 from "../controllers/v2/articles.js";

const handlers = { "1": v1, "2": v2 };

export function versionMiddleware(ctx, next) {
  const version = ctx.get("API-Version") || "1";
  const ctrl    = handlers[version];
  if (!ctrl) ctx.throw(400, `Unknown API version: ${version}`);
  ctx.state.ctrl = ctrl;
  return next();
}

Routes then delegate to ctx.state.ctrl:

router.get("/articles", versionMiddleware, async (ctx) => {
  await ctx.state.ctrl.listArticles(ctx);
});

Trade-offs

ApproachProsCons
URL prefixBookmarkable, cacheable, visibleDuplicated URL space
Accept headerClean URLs, REST-pureHarder to test in browser, less visible
Custom headerFlexible, easy to addNon-standard, not cache-friendly

URL prefix is the most practical default for public APIs. Header versioning suits internal APIs where you control all clients.

Up Next

Any versioning strategy will eventually surface errors. The next lesson builds a consistent JSON error response shape for the whole API.

Error Handling →