Mahamudul Hasan Rubel
HomeAboutProjectsSkillsExperienceBlogPhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • About
  • Projects
  • Skills
  • Experience
  • Blog
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
ArchitectureJune 22, 20265 min read

API Design: Implementing Versioning via Custom Request Headers

API design with custom request headers enables cleaner URI structures and smoother evolution. Learn how to manage versioning without breaking client contracts.

API designREST APIsoftware architecturebackend engineeringAPI versioningmicroservicesAPIArchitectureBackendSystem Design

When I first started building distributed systems, I thought versioning via the URL path (e.g., /v1/users) was the gold standard. It’s visible, easy to cache, and dead simple to route in Nginx. But after wrestling with massive API suites for a few years, I realized that URL versioning often leads to a bloated, unmaintainable mess.

If you’re looking to clean up your endpoints, implementing API design through custom request headers is a powerful, albeit opinionated, shift. It decouples your resource identifiers from the implementation version, letting you evolve your schema without forcing clients to rewrite their entire URI construction logic.

Why Move Away from URL Versioning?

The biggest problem with /v1/ or /v2/ in a path is that it isn’t actually a resource identifier—it’s an implementation detail. When you move to a new version, you aren't just changing a version number; you're often changing the underlying data shape or business logic.

We once spent about two weeks refactoring a legacy service because our URL versioning strategy forced us to duplicate every single controller route. It was a nightmare to maintain. By switching to a custom header like X-API-Version, we found that we could keep our resource URIs clean and permanent. If you're interested in the broader context of how these choices impact your system's lifespan, check out my thoughts on REST API design choices that scale without technical debt.

Implementing Header-Based Versioning

The strategy is simple: instead of changing the endpoint, you change the contract metadata sent by the client. In a Node.js or Go backend, you check for the header early in the middleware layer.

Here’s how a standard request looks:

HTTP
GET /users/123 HTTP/1.1
Host: api.example.com
X-API-Version: 2023-10-01

In your application code, you can use a simple handler to route this to the correct service logic:

JAVASCRIPT
// A simplified Express middleware example
app.use((req, res, next) => {
  const version = req.headers[CE9178">'x-api-version'] || CE9178">'2023-01-01';
  
  if (version === CE9178">'2023-10-01') {
    return userController.v2(req, res);
  }
  return userController.v1(req, res);
});

Using this approach for REST API versioning means your client code stays focused on the resource, not the versioning scheme. You don't have to worry about changing the client’s base URL every time you roll out a minor breaking change. For a deeper dive into why this is often superior to path-based approaches, see my previous guide on REST API design: Mastering Header-Based Versioning for Clean Evolution.

The Trade-offs of API Contract Management

Of course, nothing is free. When you move versioning into the headers, you lose some of the "at-a-glance" visibility that path-based versioning provides. Caching also becomes slightly more complex.

If you’re using a CDN like Cloudflare or Fastly, you must ensure your cache keys include the X-API-Version header. If your team ignores this, you’ll end up serving version 1 data to a version 2 client, which usually results in a 500 error or, worse, silent data corruption.

When you prioritize API contract management, you need to treat the version header as part of the primary key for your cache. I’ve seen teams lose roughly 15% of their latency improvements because they forgot to configure their cache headers correctly after shifting to a custom header strategy.

Best Practices for Long-Term Stability

If you commit to this path, stick to these rules:

  1. Use Date-Based Versioning: Avoid v1, v2. Use dates like 2023-10-01. It signals exactly when that contract was finalized and helps you track the age of your legacy support.
  2. Default Gracefully: Always define a default version in your middleware if the header is missing. This prevents your entire API from breaking when a client forgets to include the header.
  3. Document via Content Negotiation: Even if you use headers, document them clearly in your OpenAPI/Swagger specs. Mention that the header is mandatory for specific features.
  4. Monitor Usage: Log the version headers in your observability stack. You’ll be surprised to see how long some clients cling to old versions—sometimes for years.

If you’re managing complex systems where you need to maintain backward compatibility across multiple iterations, it's worth reviewing the core principles of API Versioning Strategies: Maintaining Backward Compatibility at Scale to ensure you aren't creating unnecessary friction for your consumers.

FAQ

Q: Does header-based versioning break standard browser caching? A: Yes, it can. Since browsers don't natively know that the X-API-Version header changes the response, you must ensure your API sends the Vary: X-API-Version header. This tells caches that the response is unique to that specific header value.

Q: Is it better to use Accept headers instead of custom X- headers? A: Technically, the Accept header (using application/vnd.myapp.v2+json) is more "RESTful" because it follows content negotiation standards. However, custom headers are often easier to debug and parse in middleware. I prefer custom headers for internal microservices and Accept headers for public-facing APIs.

Q: How do I deprecate an old version? A: Use a Warning or Deprecation header in your response. Even if the client is using an old version, you should proactively inform them that the version is reaching end-of-life.

At the end of the day, there’s no perfect architecture. I’ve moved teams back to URL versioning when they realized their tooling couldn't handle complex header-based routing. Choose what fits your team's operational maturity, not just what looks cleanest in a blog post. I’m still experimenting with how to better automate the deprecation of old headers, as that remains the most manual part of the process.

Back to Blog

Similar Posts

Close-up of wooden blocks spelling 'REPLY' on a table with teal background.
ArchitectureJune 21, 20264 min read

API Performance: How to Implement Request Hedging for Lower Tail Latency

API performance depends on managing tail latency. Learn how to implement request hedging to fire redundant requests and keep your distributed system fast.

Read more
ArchitectureJune 22, 2026
4 min read

API Traffic Shadowing: Validate New Services Without Production Risk

API traffic shadowing lets you test new code against real-world production data without impacting users. Learn how to implement it safely and reliably.

Read more
ArchitectureJune 21, 20264 min read

REST API Resource Partial Updates: JSON Patch vs. Merge Patch

REST API resource partial updates require choosing between JSON Patch and Merge Patch. Learn how to weigh their trade-offs to ensure long-term maintainability.

Read more