Get started with 33% off your first certification using code: 33OFFNEW

How to version your API to allow breaking changes

6 min read
Published on 29th March 2023

Blog Image

API versioning is essential for maintaining compatibility between different versions of an API while allowing developers to make improvements, enhancements, and bug fixes. In this article, we'll discuss various strategies for versioning a REST API and demonstrate how to implement them using JavaScript and Node.js. We'll build a simple versioned REST API using the Express framework as an example.

When you have a versioned API it allows you to introduce breaking changes, whilst maintaining the old API for existing customers. You may wish to retire old API versions, but maintaining a versioning system allows you to transition your users over time. This article discusses the various strategies available, but the examples used are kept deliberately simple. It is up to you to decide how to structure your code to ensure it's easily maintainable. Most of our examples use simple if-statements to easily explain the concepts, but you'll likely want to use a more robust approach and abstract your code appropriately.

API Versioning Strategies

There are several common strategies for versioning REST APIs, including:

  1. URI Versioning: This approach includes the API version in the request URI, e.g., /api/v1/users or /api/v2/users.
  2. Query Parameter Versioning: The API version is included as a query parameter in the request URL, e.g., /api/users?version=1 or /api/users?version=2.
  3. Custom Request Header Versioning: A custom request header is used to indicate the API version, e.g., X-API-Version: 1 or X-API-Version: 2.
  4. Content Negotiation (Accept Header) Versioning: The client specifies the API version using the Accept header, e.g., Accept: application/vnd.example.v1+json or Accept: application/vnd.example.v2+json.

Each of these strategies has its pros and cons, and the choice largely depends on the specific requirements and constraints of your API. You may also notice that these strategies are all similar to strategies you would use for authorization within an API, which is a separate topic, but you may wish to keep the strategy the same for both to make things easier for users.

As your API grows in both features and popularity you may choose to create an SDK for popular languages. Usually when this happens the strategy implemented essentially becomes invisible for your customers, as it is simply defined in the SDK itself, and users won't be interacting with URLs directly.

Setting Up the Project

To begin, create a new Node.js project and install the necessary dependencies. We're assuming here that you have Node.js and npm installed and available globally.

mkdir versioned-rest-api
cd versioned-rest-api
npm init -y
npm install express

Next, create an index.js file in the project root directory and import the required dependencies:

const express = require('express');

const app = express();
const PORT = process.env.PORT || 3000;

// API routes and versioning strategies will be implemented here

app.listen(PORT, () => {
    console.log(`Server listening on port ${PORT}`);
});

This code should be fairly self-explanatory. We've created a basic web server which listens on port 3000 (unless you specify it in an environment variable). This is the code we're going to use as the shell for our API.

Implementing URI Versioning

To demonstrate URI versioning, let's create two different versions of a simple API endpoint for fetching user data:

app.get('/api/v1/users/:id', (req, res) => {
    const user = {
        id: req.params.id,
        name: 'John Doe'
    };
    res.json(user);
});

app.get('/api/v2/users/:id', (req, res) => {
    const user = {
        id: req.params.id,
        fullName: 'John Doe',
        email: '[email protected]'
    };
    res.json(user);
});

With URI versioning, clients can access different versions of the API by simply changing the version number in the request URL, such as /api/v1/users/1 or /api/v2/users/1.

As you can see here version 2 of the API has introduced a breaking change. name is now being returned as fullName which will break any apps communicating with the API and using that variable, and we've introduced email as a field. Generally speaking, once an API has been published no changes would be made to it unless it is fixing a bug.

Implementing Query Parameter Versioning

To implement query parameter versioning, create a single API endpoint and use the version query parameter to determine which version of the API to serve:

app.get('/api/users/:id', (req, res) => {
    const version = req.query.version;

    if (version === '1') {
        const user = {
            id: req.params.id,
            name: 'John Doe'
        };
        res.json(user);
    } else if (version === '2') {
        const user = {
            id: req.params.id,
            fullName: 'John Doe',
            email: '[email protected]'
        };
        res.json(user);
    } else {
        res.status(400).send('Invalid API version specified');
    }
});

Clients can access different versions of the API by changing the version query parameter in the request URL, such as /api/users/1?version=1 or /api/users/1?version=2.

Implementing Custom Request Header Versioning

To implement custom request header versioning, create a single API endpoint and use the X-API-Version header to determine which version of the API to serve:

app.get('/api/users/:id', (req, res) => {
    const version = req.header('X-API-Version');

    if (version === '1') {
        const user = {
            id: req.params.id,
            name: 'John Doe'
        };
        res.json(user);
    } else if (version === '2') {
        const user = {
            id: req.params.id,
            fullName: 'John Doe',
            email: '[email protected]'
        };
        res.json(user);
    } else {
        res.status(400).send('Invalid API version specified');
    }
});

Clients can access different versions of the API by changing the X-API-Version header in their request, e.g., X-API-Version: 1 or X-API-Version: 2.

Implementing Content Negotiation (Accept Header) Versioning

To implement content negotiation versioning, create a single API endpoint and use the Accept header to determine which version of the API to serve:

app.get('/api/users/:id', (req, res) => {
    const acceptHeader = req.header('Accept');

    if (acceptHeader === 'application/vnd.example.v1+json') {
        const user = {
            id: req.params.id,
            name: 'John Doe'
        };
        res.json(user);
    } else if (acceptHeader === 'application/vnd.example.v2+json') {
        const user = {
            id: req.params.id,
            fullName: 'John Doe',
            email: '[email protected]'
        };
        res.json(user);
    } else {
        res.status(400).send('Invalid API version specified');
    }
});

Clients can access different versions of the API by changing the Accept header in their request, e.g., Accept: application/vnd.example.v1+json or Accept: application/vnd.example.v2+json.

Popular APIs and their approaches

Lets take a look at some popular APIs and the approach they have opted for.

  • Stripe uses the URI approach, for example /v1/payouts.
  • Facebook/Meta uses the URI approach, for example /v11/me
  • Google Maps uses a mixture of URI and query string approaches
  • Twitter uses the URI approach, but interestingly didn't implement it initially, so v1 is at /tweets but v2 is at /2/tweets
  • Amazon AWS uses URI approach

As you can tell, the URI approach is by far the most popular versioning strategy available, probably due to its visibility (it's always obvious which API version you're using), and the ease of access to URI segments within popular frameworks when compared to headers and query strings, which often aren't as accessible in different parts of the lifecycle within a framework (in other words, many frameworks don't expose those variables in the router).

Conclusion

In this article, we've explored various strategies for versioning a REST API and demonstrated how to implement them using JavaScript and Node.js with the Express framework. Each strategy has its advantages and drawbacks, and the choice depends on your specific API requirements and constraints.

It is also important to consider what strategies you're using for other selection criteria from your users. For example, how does a user authenticate and pass authorization tokens to you? It's you've implemented Bearer tokens via a header then the header method of API versioning is probably the cleanest method. Your framework may have poor support for header handling, so perhaps you would opt for URI-based versioning, or it might be that you wish to handle API versioning in your framework router, in which case URI-based versioning is also the best choice. The decision really comes down to your specific circumstances and your design approach within your application.

By implementing versioning in your API, you can ensure that clients can continue using older versions while you make improvements and enhancements to newer versions. This allows for a smoother transition and prevents breaking changes from affecting existing clients.