Header versioning
Version in the headers
The previous lesson put the version in the URL. This approach goes the other direction: put the version in a request header instead.
GET /books HTTP/1.1
Accept: application/vnd.catalog.v2+json Or use a custom header:
GET /books HTTP/1.1
X-API-Version: 2 The URL stays clean. /books is always /books, regardless of version. The version becomes metadata, separate from the resource identifier.
How it works
The server reads the version from the header and routes to the right handler:
route.get("/books", {
resolve: ({ request }) => {
const version = getApiVersion(request);
if (version === 2) {
return getV2Books();
}
return getV1Books(); // default
},
});
function getApiVersion(request: Request): number {
// Custom header approach
const header = request.headers.get("x-api-version");
if (header) return parseInt(header, 10);
// Accept header approach
const accept = request.headers.get("accept") ?? "";
const match = accept.match(/vnd\.catalog\.v(\d+)\+json/);
if (match) return parseInt(match[1], 10);
return 1; // default to v1
} Let’s walk through this. The getApiVersion function checks two places. First, it looks for a custom X-API-Version header. If that’s there, it parses the number and returns it. If not, it checks the Accept header for a vendor media type pattern like vnd.catalog.v2+json. If neither is present, it defaults to version 1.
The route itself is a single URL, /books, that branches internally based on whatever version the header specifies. One route, one URL, different behavior depending on the header.
The Accept header convention
The formal approach uses the Accept header with a vendor media type:
Accept: application/vnd.catalog.v1+json
Accept: application/vnd.catalog.v2+json vnd means “vendor,” meaning it’s your custom type. catalog is the name of your API. v2 is the version. +json means the body is JSON. This follows RFC 6838.
GitHub’s API uses this approach: Accept: application/vnd.github.v3+json.
The advantages
Clean URLs. /books/book-1 is always /books/book-1. No version in the path. URLs identify resources, headers specify how to represent them.
Gradual migration. A client can switch versions by changing one header instead of updating every URL in their codebase. That’s a much smaller change.
The disadvantages
Not visible. You can’t see the version by looking at a URL. Debugging requires inspecting headers. Sharing an API call in documentation or a chat message requires showing the header too, not just the URL.
Caching complexity. /books now returns different content depending on the header. CDNs need Vary: Accept or Vary: X-API-Version to cache correctly. Without that, a CDN might serve a v1 response to a v2 client, or the other way around.
[!NOTE] The Caching course covered
Varyheaders briefly. With header versioning, every cached endpoint needsVary: X-API-Version(orVary: Accept) to ensure the cache serves the correct version.
Harder to test. curl https://api.example.com/books doesn’t tell you what version you’ll get. You need curl -H "X-API-Version: 2" https://api.example.com/books. Browsers can’t easily add custom headers either, so quick testing in the address bar won’t work.
Default version ambiguity. What happens when no version header is sent? Do you default to v1? Return an error? Both are reasonable choices, but the decision has to be made and documented. With URL versioning, there’s no ambiguity: /v1/books is v1, always.
There’s one more approach to look at: putting the version in a query parameter. It’s the simplest of the three, but that simplicity comes with its own set of problems.
Exercises
Exercise 1: Add a getApiVersion helper that reads X-API-Version. Use it in a route to return different responses.
Exercise 2: Call the endpoint without the header. Verify it defaults to v1. Call with X-API-Version: 2. Verify it returns v2.
Exercise 3: Add Vary: X-API-Version to the response. Explain why this matters for CDN caching.
What is the main disadvantage of header versioning?