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.
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
| Approach | Pros | Cons |
|---|---|---|
| URL prefix | Bookmarkable, cacheable, visible | Duplicated URL space |
| Accept header | Clean URLs, REST-pure | Harder to test in browser, less visible |
| Custom header | Flexible, easy to add | Non-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 →