API versioning
There are lots of ways to implement API versioning. Picking one is an exercise in balance: what voices will you listen to?
For us, the most important voice is the voice of the customer. There are lots of customers, though, and many of them just want to get the job done. They are expecting us to be the experts and make it easy for them to build integrations that Just Work™.
People using our APIs don’t really care whether we’re 100% Fielding-Certified REST™ (not a real thing). They want something that’s simple and predictable.
How can we generally meet people where they’re at, and give everyone the benefit of versioning without creating work for them?
I consider Stripe’s API versioning strategy to be the gold standard. If you read their post, you’ll see how they carefully balance the ideal of “we will never break your integration with us” with “sometimes we need to make breaking changes”.
When do we need a new version?
Most people agree that you need a new API version every time you have a breaking change.
What’s a breaking change? It depends. Some people are really strict: if there is a new attribute, that’s a breaking change in their minds. I tend to be more lenient. If an attribute changes type (for example, from a simple type to an object) or if a required attribute in a response goes away, that’s a breaking change. It gets a little trickier when you modify an enumerated type, so you need to understand your audience and how they are using your API.
What’s a good version string look like?
The Stripe folks use a date to represent an API version; it’s handy as people generally understand how to compare dates. Some people get confused when they see an “old” API version and think that your documentation is out of date, but those people are rare.
Other folks prefer to use semantic versioning (v1.0.3
), or use a simple string like v1
and move on.
I feel like v1
isn’t particularly granular, semantic versioning is a bit sketchy from an API perspective and too much like a release number, and prefer date strings.
Options
Just put a version in the URL
also known as “What’s wrong with /v1/widgets/123456
?”
Some people love putting the API version in the path portion of the URL. I don’t particularly like this myself. If I have one resource one day that has a breaking change, then I have to break out /v2
. The next day, another breaking change to a resource, /v3
. Where does it end? What do you do with the resources that didn’t change?
From a dogmatic perspective, putting the API version in the URL path means that your resources at /v1/*
and at /v2/*
and at /v3/*
are different things, not different representations of the same things. People with a Job To Be Done won’t care, but it’s a bit irksome to have something so flagrantly against how the Web is supposed to work. It’s handy from a service developer’s perspective, as you can “just” put a new endpoint handler in place, but if only one resource has changed then it gets messy.
Use content negotiation like Fielding intended
also known as “What’s wrong with Accept: application/x-widget+json;v=1
? Accept: application/x-widget.v1+json
?”
For a long time I believed that content negotiation was Totally Awesome™. Imagine: something built into the HTTP standards that lets you not only discover what potential representation options exist for any given resource, but also transparently negotiate upgrades on either the client or the server side.
I was so enamoured with this idea I did a conference talk on it, claiming that using content negotiation was level π of the Richardson Maturity Model (because it’s transcendent and slightly greater than 3).
I’ve mellowed since then. I still believe that content negotiation is an awesome mechanism, but it’s a bit too clever for some folks, and unfortunately framework support is rather sparse for parameterized content types on both the client and server side.
Forcing people to learn about content negotiation before they can use your API also means that some people will just walk away.
Use an API version header
also known as “What’s wrong with Api-Version: 2020-04-01
?”
I haven’t managed to find any concrete reasons why you shouldn’t use a custom header for passing the API version. The best people have been able to give me is “it’s harder to set up header-based routing”, which is sometimes true and totally valid for people in that position.
If you allow caching of API responses, you should include a Vary: Api-Version
header in your responses if you go this route; it will tell any caching infrastructure that the response may be different if the API version in the request is different.
The Stripe folks have a really smart mechanism: if you don’t specify the API version in your request, they look at what API version your account is pinned at. If it’s your first time calling the API, they pin you to the current version. You can change your pinned API version through the console (and presumably through the account management API as well).
I haven’t yet gone so far as to implement API version pinning. The best I’ve been able to do is default to the latest version when the header isn’t present – this gives folks the most chance of seeing responses that match the current API docs, but it means that folks who don’t provide an API version header are more vulnerable to breaking changes.