How to Version Your REST API Properly

We earn commissions when you shop through the links below.

If you’ve ever broken a client integration by changing an API response shape, you already understand why knowing how to version your REST API properly is not optional — it’s a professional necessity. API versioning is one of those topics that feels simple until you’re maintaining three versions simultaneously, dealing with deprecation pressure, and fielding angry emails from developers whose apps stopped working. Let’s cut through the noise and look at what actually works.

Why API Versioning Matters

APIs are contracts. The moment you make your API public — or even share it with another team — you’ve made a promise about how it behaves. Changing that behavior without warning breaks trust and breaks code. Versioning gives you a controlled way to evolve your API without pulling the rug out from under existing consumers.

The stakes get higher when you’re running microservices across multiple environments. Hosting your backend on a platform like DigitalOcean makes it easy to spin up separate environments per API version, but you still need a clear versioning strategy before your infrastructure decisions matter.

The Three Main Versioning Strategies

1. URL Path Versioning

This is the most common approach and the one I recommend for most teams. The version lives directly in the URL:

GET /api/v1/users
GET /api/v2/users

Pros:

  • Immediately visible in logs, browser history, and documentation
  • Easy to route at the load balancer or reverse proxy level
  • Simple for clients to understand and implement

Cons:

  • Violates the REST principle that a URI should represent a unique resource
  • Can lead to URI sprawl if not managed carefully

I’ll take pragmatic over theoretically pure any day. URL versioning wins for developer experience.

2. Header-Based Versioning

The version is passed in a custom request header:

GET /api/users
Accept-Version: v2

# or using the Accept header
GET /api/users
Accept: application/vnd.myapp.v2+json

Pros:

  • Keeps URLs clean
  • More RESTfully correct

Cons:

  • Harder to test in a browser or simple HTTP client
  • Easy to forget, leading to silent failures
  • Adds complexity to caching strategies

3. Query Parameter Versioning

GET /api/users?version=2

I’d avoid this one. It’s messy, easy to strip by proxies, and feels like an afterthought. You see it in older APIs that bolted versioning on after the fact. Don’t start here.

A Real-World URL Versioning Implementation

Here’s how I structure versioned routes in an Express.js API. The pattern scales well and keeps version logic isolated:

// routes/v1/users.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  res.json({
    users: [
      { id: 1, name: 'Alice', email: 'alice@example.com' }
    ]
  });
});

module.exports = router;

// routes/v2/users.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  // v2 returns paginated results with metadata
  res.json({
    data: [
      { id: 1, name: 'Alice', email: 'alice@example.com', createdAt: '2023-01-15' }
    ],
    meta: {
      total: 1,
      page: 1,
      perPage: 20
    }
  });
});

module.exports = router;

// app.js
const express = require('express');
const app = express();

const usersV1 = require('./routes/v1/users');
const usersV2 = require('./routes/v2/users');

app.use('/api/v1/users', usersV1);
app.use('/api/v2/users', usersV2);

app.listen(3000, () => console.log('API running on port 3000'));

Each version has its own router module. Shared business logic lives in service files that both versions import. This way you’re not duplicating database queries — just the HTTP response shape.

Versioning Best Practices

Use Major Versions Only

Don’t version patch changes or minor additions. If you add a new optional field to a response, that’s backward compatible — ship it without bumping the version. Reserve version bumps for breaking changes: removing fields, changing data types, restructuring response shapes, or altering authentication mechanisms.

A good rule of thumb: if an existing client would need to change code to handle your change, it’s a breaking change. Version it.

Version from Day One

Even if you only have /api/v1 and you never plan to change anything, start with versioning. It costs almost nothing upfront and saves enormous pain later. I’ve seen teams scramble to retrofit versioning onto production APIs and it’s never pretty.

Deprecate, Don’t Delete

When you release v2, don’t immediately kill v1. Establish a clear deprecation timeline — 6 to 12 months is reasonable for most teams. Communicate it clearly in your documentation and via response headers:

HTTP/1.1 200 OK
Content-Type: application/json
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Deprecation: true
Link: <https://api.example.com/api/v2/users>; rel="successor-version"

The Sunset header (RFC 8594) is a standard way to tell clients exactly when a version goes away. Use it.

Document Every Version

Your documentation needs to clearly show what changed between versions and provide a migration guide. If you’re using OpenAPI/Swagger, maintain separate spec files per version. Lazy documentation is how you end up with clients still on v1 in 2028 because they never knew v2 existed.

If you want to sharpen your API design skills more broadly, Udemy has solid courses on REST API design and documentation that cover OpenAPI specs in depth.

What About Internal APIs?

A common mistake: skipping versioning for internal APIs because “only we consume them.” This breaks down the moment you have multiple frontend teams, multiple mobile app versions in the wild, or any automated workflow consuming your API. If you use a tool like Make to automate workflows that hit your internal endpoints, an unversioned breaking change will silently kill those automations.

Treat internal APIs with the same discipline as public ones. Future-you will be grateful.

Common Mistakes to Avoid

  • Versioning too granularly. Don’t create v1.1, v1.2, v1.2.1. Stick to major versions.
  • Forking too much logic. If v1 and v2 handlers share no code, you have a maintenance nightmare. Extract shared services and transform at the response layer only.
  • No sunset communication. Telling nobody about deprecation then pulling the plug is how you become infamous in developer forums.
  • Changing behavior without a version bump. Fixing a bug is fine. Changing what a field means is a breaking change, even if the field name stays the same.

My Recommended Approach

For the vast majority of projects, here’s what I’d do: use URL path versioning with major versions only, start at v1 on day one, maintain a changelog, and give at least 6 months notice before sunsetting any version. That combination covers 95% of the complexity without over-engineering anything.

Knowing how to version your REST API properly comes down to discipline and communication as much as technical implementation. The routing code is the easy part. The hard part is committing to a deprecation policy and actually communicating it to your consumers. Get that part right and the rest follows naturally.

Wrapping Up

Understanding how to version your REST API properly is a career-level skill, not just a code pattern. APIs that break clients erode trust, and trust is hard to rebuild. Start with URL versioning, keep shared logic in service layers, communicate deprecations early with Sunset headers, and document everything. Your API consumers — and your future self — will thank you.