hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses API versioning and evolution with @hectoday/http

Why versioning

  • Breaking changes
  • The versioning contract
  • Project setup

Versioning strategies

  • URL path versioning
  • Header versioning
  • Query parameter versioning
  • Choosing a strategy

Building versioned APIs

  • Side-by-side versions
  • Version routers
  • Validation per version

Evolving without breaking

  • Additive changes
  • Deprecation
  • Field renaming and removal
  • Changing response shapes

Lifecycle management

  • Sunset policies
  • Monitoring version usage
  • Checklist

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 Vary headers briefly. With header versioning, every cached endpoint needs Vary: X-API-Version (or Vary: 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?

← URL path versioning Query parameter versioning →

© 2026 hectoday. All rights reserved.