Supporting multiple API versions

Building a web application with a backend API and multiple clients is great. Everyone is using the same API and you can allow even other people to build clients, while you focus on the core. Like Twitter used to be.

Not only that, but you can also improve your backend speed, or change your infrastructure without affecting your clients and without changing your client side code.

Problems

Then it comes the day when you want to change something which will break the API and will require every client to be updated, and if there is an iOS version, you will have to wait for 1-2 weeks before the update actually happens for your users. You are probably asking yourself some those questions:

  • Should I really make that change?
  • Shoud I have different paths for different API versions?
  • Can I keep the same paths and still support multiple versions?
  • Is there a way that even clients using an old version can benefit from some of the improvements?
  • How should I structure my code and folders?
  • Where should I put different API versions?
  • If I use different paths for different versions should I change every path?
  • What if my current large app has no support for multiple API versions?
  • How should I handle legacy code? When can I remove it safely?
  • What if there is a client who never updates?

These are legitimate questions and most developers have to deal with them sooner or later. I will show you two solutions on how to deal with multiple API versions. The first one is using different path prefixes for different versions. The second doesn’t changes paths but instead transforms requests. We will look at both their advantages and disadvantages.

Should I make that change?

Before talking about the two solutions for multiple API versions, let’s first see the third solution. It’s to avoid making incompatible API changes.

There are different kinds of API changes. Some can be implemented as an additional option to some of your current API. In those cases, you don’t need anything else. These are the changes that are the easiest to handle. For example:

'/users'

will return all existing users. If you only want to add sorting, all you have to do is add an option.

'/users?sort=name'

All clients which don’t know about the sorting will continue to work as usual.

However, sometimes adding an option is not appropriate, even when the change affects only one path. A better solution in these cases might be to create a new route.

For example, if we build an admin section, we would want our app to return much more information accessible only to special users. Using the same path may introduce unnecessary complexity and security issues. Therefore implementing a different path might be a better solution.

'/admin/users'

The existing API is still intact, so clients who don’t know about the new path will continue to work.

You will be able to avoid making incompatible changes to your API in lot’s of cases by following the two simple solutions above, but at some point you may end with paths which have thousands options, some of them rarely used and others who are obsolete. Also you may end up with many different routes which are very similar to one another.

All of this can result in a nightmare to maintain. Moreover, it can be really difficult to understand by developers writing clients for your API. They might easily confuse which options and which similar paths should they use.

Path prefixes for different versions

The first solution that I will show you is to use different path prefixes for different API versions. It’s very common to use ‘v1’ for your first version, ‘v2’ for your second one and etc.

Let’s look at an example:

'/v1/users'
'/v1/cars'

'/v2/users'
'/v2/bicycles'

Here we see two versions of an API. The first one is prefixed with ‘v1’ and the second with ‘v2’. What’s more the two version are clearly incompatible as the first one contains the ‘cars’ path and the second doesn’t, also the second contains the ‘bicycles’ path but the first doesn’t.

Prefixing is like semantic versioning, going from ‘v1’ to ‘v2’ for many parts of the API will break backward compatibility.

Let’s look at another example, but this time we will just change the requests for the different API versions.

'/users'

The original API has options for sorting, limiting, pagination and searching but can return at most 100 users a time as otherwise the query becomes too slow. Let’s create a second version.

'/v2/users'

In the second version the users always come sorted by their name and you can request users in alphabetical range. For example, all users with names between Jake and John. However, now you can request 100,000 users at a time and the request will always be very fast.

Path prefixes and your code and files

Now that you have a solution, what will be the appropriate way to organize your code and files?

Depending on the size of your API and the size of your changes, I would suggest between two possible solutions.

You can keep both implementations in the same files side by side. This will work if your implementations are small and if there are not too many paths. They can even share some code.

Another possible structure is to have two sub folders, for example v1 and v2. Each will contain whatever they need, even some identical code. The result might be bigger overall size, but much easier to maintain. You can even have different teams maintaining different versions.

Here we are talking mainly about the controllers serving the paths because this is what the outside clients of the API are seeing. However, all of this can be also true for your models. In fact, in some extreme cases v1 and v2 can be two completely different apps each with its own models and controllers and even written in different languages and served from different servers.

Path prefixes advantages and disadvantages

Using different paths is a nice solution, but it has it’s disadvantages, too.

The first one is that it doubles your code. When you have similar paths you will probably do some similar things inside each path. This makes your code harder to maintain.

Another inconvenience is when you only need partial changes. What if you have two paths and you want to keep one and to completely change the other? You may try by adding a new path, but as we’ve talked earlier as your app grows this is hard to do, too. You can also have a ‘v2’ prefix for only the changed path or maybe have the ‘v2’ for both paths and the unchanged path will be served by the original code, too.

Don’t be quick to dump path prefixing as it has its bright sides, too.

For instance, it makes it very clear and explicit which app version your client is using. This can help immensely when something goes wrong.

Another huge advantage is when big changes to an API are required. What was one considered cutting edge API and loved by all, today might be outdated and clunky and even frustrating for developers (I am looking at you old PayPal API). So, instead you can just put under ‘v2’ a completely new modern API inspired by the best practices around. It’s extremely easy to put a completely new API without affecting anyone currently using the old.

Request transformation for different versions

Another solution to making backwards incompatible changes to an API is to use request transformations. Actually, in this scenario every API change still has its own version.

Let’s look again at the example that we saw earlier

'/users'

The original API has options for sorting, limiting, pagination and searching but can return at most 100 users a time as otherwise the query becomes too slow. Let’s create a second version.

'/users'

In the second version the users always come sorted by their name and you can request users in alphabetical range. For example, all users with names between Jake and John. However, now you can request 100,000 users at a time and the request will always be very fast.

However, you still have the same path as before. To make it work we have two critical points.

First is how you know which version to handle. Usually, the user will be able to explicitly tell which version he is interested in by using a header or appending a parameter to the query request which specifies the API version. However, this is not very convenient and can be easily missed. Instead the client can have an admin area where his default API version can be set and his client code won’t need to provide an explicit version.

The second thing in this solution is that after we know the version, how do we handle each version internally. Do we have different request handlers? No. As the name of this solutions says, instead we will transform the request.

Before our handling code receives the request, we will use a transformation which will make the request from any version to look like a request from the latest API version. Then after our business logic is done, we will process the result so that it will look like a result from the client specified version.

This way there is a clear separation in our code and there is only one place which contain business logic. Any improvements there will automatically affect all API versions.

To handle this requests most web frameworks provide some sort of mechanism to process the request before handling it to the actual request handler for the specific path. In the case of Express, these are express middlewares.

Request transformation and when do you increase the API version

In this solution there are still API versions. So there is still the question when we increase the version. The answer is easy, it is each time we make an backwards incompatible change. It may affect just a few paths and leave the rest untouched.

However, try to push incompatible changes in batches. Otherwise you may end up with 1000 different versions, which will be again nightmare to handle.

Request transformation and your code and files

Organizing your code is very easy. You have your business logic at only one place and your transformation logic also at only one place, so you can keep the same structure as if you had only one version.

Request transformation and disadvantages

As in the first solution, not everything is roses. There are cases in which this handling of multiple versions is not very convenient.

For example, when drastic changes to the API are required simple request transformations might not be enough or they might become too complicated. In such cases path prefix versioning is a better solution.

Another problem is that it is a little bit more inconvenient when the user handles different API versions.

On the other side you have some very attractive advantages.

Your business logic is only one, so any bug fixes or changes are done at only one place and then all your clients, no matter which version they use, will benefit from them. This simplifies maintenance and file and code structure.

When is it safe to remove a legacy version of the API?

No matter which solution you choose, there is always the problem that supporting many versions of your API makes your code harder to maintain. Sometimes it is a reasonable to drop some API versions, but when?

The easy answer is when nobody uses them anymore. However, practice shows that often there are clients that never update.

The better answer is when it is no more worth it. This might mean that you will have to support a legacy API for years. Let’s look at two examples.

Basecamp (formerly 37Signals) has two version of their primary app: Basecamp and Basecamp Classic. Basecamp Classic hasn’t been updated for the last several years with the exception of critical security fixes. Which, as you imagine, is very rare. Basecamp Classic is responsible for half of their business at the time of writing this essay, which is probably millions per month. So in return of rare and small security fixtures, you are paid handsomely. Just because something is old and not so good, doesn’t mean that it has to disappear.

Stripe customers will literally loose money if one of the API versions stops working. Stripes knows that their customers expect that an app with their API should work and accept payments forever and they build their API so that it can work forever.

Mixed solution

The two solutions above are not the only ones. You can create any number of other solutions especially by combining ideas from both of them. This way you can benefit from the strong sides of both and probably avoid their weaknesses.

Next

Now that you’ve learned about handling multiple API versions is time for practice. Try to implement a small change first using the prefixes solution and then using the request transforming solution. Afterwards, you may even try to combine both and create your own hybrid solution.

Did you like this article?

Please share it

We are Stefan Fidanov & Vasil Lyutskanov. We share actionable advice about development with Node, Express, React and other web & mobile technologies.

It is everything that we have learned from years of experience working with customers from all over the world on projects of all sizes.

Let's work together
© 2024 Terlici Ltd · Terms · Privacy