Your API is a product. And like any product, it will need to change and evolve over time. You'll add new features, change data structures, and deprecate old functionality. But what happens to all the client applications that are already using your API? If you make a change, you might break them all. 😱

This is where API versioning comes in. It's the practice of managing changes to your API so that you can introduce new features without disrupting existing users.

Why Version Your API?

The short answer: to prevent breaking changes. A breaking change is any modification that could cause a client application relying on the previous behavior to fail.

Examples of breaking changes:

  • Removing a field from the JSON response.
  • Changing the data type of a field (e.g., from a number to a string).
  • Adding a new, mandatory field to a request body.
  • Changing the URL of an endpoint.

Versioning allows you to release these breaking changes in a new, separate version (v2), while allowing older clients to continue using the stable, old version (v1).

Common Versioning Strategies

There are three popular ways to specify the API version in a request.

  1. URI Path Versioning (Most Common) This is the most straightforward and widely used method. You simply include the version number in the URL path.
  • Example: https://api.example.com/v1/users
  • Pros: Very explicit. It's clear to anyone looking at the URL which version is being used. Easy to route in server-side code.
  • Cons: Some argue it violates the principle that a URI should refer to a unique resource.
  1. Query Parameter Versioning The version is specified as a query parameter in the URL.
  • Example: https://api.example.com/users?version=1
  • Pros: Simple to implement.
  • Cons: Can make request logs messy and is easier for a client to omit by mistake than a path segment.
  1. Custom Header Versioning The version is specified in a custom HTTP header.
  • Example: Accept: application/vnd.example.api.v1+json
  • Pros: Considered the "purest" REST approach as the URI (/users) remains uncluttered and points to the same resource, with the version being part of the content negotiation.
  • Cons: Less discoverable. It's not visible in the browser's address bar, making it harder to test and debug manually.

Code Snippet: URI Versioning in Express.js

Here’s how you could structure your routes in an Express.js app to handle v1 and v2 of a /users endpoint.

JavaScript


import express from 'express';
const app = express();

// ----- V1 Routes -----
// GET /v1/users returns { id, name }
const v1Router = express.Router();
v1Router.get('/users', (req, res) => {
  res.json([{ id: 1, name: 'Alex Smith' }]);
});

// ----- V2 Routes -----
// GET /v2/users returns { id, firstName, lastName } - a breaking change!
const v2Router = express.Router();
v2Router.get('/users', (req, res) => {
  res.json([{ id: 1, firstName: 'Alex', lastName: 'Smith' }]);
});

// Register the versioned routers with the main app
app.use('/v1', v1Router);
app.use('/v2', v2Router);

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

The Golden Rule: Backward Compatibility

Whenever possible, you should try to make changes in a backward-compatible (or non-breaking) way. This means a client built for v1 of your API can interact with a newer version of the server code without breaking. This avoids the need for a new version number.

How to Make Non-Breaking Changes:

  • Add new, optional fields to an API response. Old clients will simply ignore them.
  • Add new, optional query parameters to a request.
  • Add new API endpoints. This won't affect existing endpoints.

When should you release a new major version (e.g., v2)? Only when you absolutely need to introduce a breaking change. If your change is additive, try to keep it within the current version.