20
Building Microservices in Go: REST APIs - Versioning
Versioning is the idea of identifying a concrete piece of software based on a value, where this value could used to reference the contract being followed by both the provider of that software as well as the user of that software.
One example, in the context of REST APIs, would be to identify the fields and types used in the payload of the same resource over time.
There are multiple ways to create this identity, from uniquely generated values to sequentially incremented numbers as well as values using release dates. In some cases special rules could be applied to those identifiers, like using odd-numbers to indicate unstable releases.
Choosing a versioning technique with clearly defined rules is important for both the team in charge of building the product as well as your users to understand how, when and if a version update is needed, this is where Semantic Versioning comes in.
Disclaimer: This post includes Amazon affiliate links. If you click on one of them and you make a purchase I'll earn a commission. Please notice your final price is not affected at all by using those links.
The way it works is by defining a versioning format consisting of three numbers with specific names and rules: X.Y.Z
, where:
-
X
: represents a major value, -
Y
: represents a minor value, and -
Z
: represents a patch value,
Each value indicates how compatible the version is compared to another one, all of this is better explained by the Semantic Versioning 2.0 Summary (emphasis mine):
Given a version number MAJOR.MINOR.PATCH, increment the:
- MAJOR version when you make incompatible API changes,
- MINOR version when you add functionality in a backwards compatible manner, and
- PATCH version when you make backwards compatible bug fixes.
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
There is no official recommendation, but different companies in the software industry like to take any of the following approaches, all of them with specific tradeoffs:
- Using a path
- Defining subdomains
- Receiving query arguments
- Accepting headers
Differentiate versions by a parent path, which identifies (most of the times) the major version, for example:
GET https://todo.app/**v1**/tasks/{taskId}
GET https://todo.app/**v2**/tasks/{taskId}
A real life example of this is the Blogger API, which at the moment defines two supported APIs with similar resources:
For example the Blogs: Get
API:
Implementation details:
- In Go: each version defines a concrete handler that happens to be using specific paths, requests and response types.
- Not Go: two different microservices using a gateway that uses the path to redirect traffic to specific versions.
- OpenAPI/Swagger: can be used.
Differentiate versions by subdomain, which identifies (most of the times) the major version, for example:
GET https://**v1**.todo.app/tasks/{taskId}
GET https://**v2**.todo.app/tasks/{taskId}
Implementation details:
- They could literally be two different microservices, totally independent of each other.
- OpenAPI/Swagger: can be used.
Differentiate versions by query arguments, with the possibility to indicate exactly the version to use, for example:
GET https://todo.app/tasks/{taskId}?**v=1**
GET https://todo.app/tasks/{taskId}?**v=2**
For example Microsoft Azure DevOps Services uses this approach where a way to specify the version would be using something like:
GET https://dev.azure.com/{organization}/_apis/{area}/{resource}?api-version=1.0
Implementation details:
- When using different technologies: a gateway could be used,
- When using the same technologies: complicated to maintain because both may be using the same underlying handler, either a
switch
-like style ormap
-based functions; or literally implement a new microservice and use a gateway, - OpenAPI/Swagger: hard to define requests and schemas, but possible, Microsoft Azure Cognitive Search: api-version does that.
Differentiate versions by values in the HTTP headers:
- Using a custom Header:
GET -H **"Version: <VERSION>"** https://todo.app/tasks/{taskId}
, or
- Using Content Negotiation, for example Github uses a vendor prefix to do so:
GET -H **"Accept: application/vnd.todo.<VERSION>+json"** https://todo.app/tasks/{taskId}
Implementation details:
- When using different technologies: a gateway could be used,
- When using the same technologies: complicated to maintain because both may be using the same underlying handler, either a
switch
-like style ormap
-based functions; or literally implement a new microservice and use a gateway, - OpenAPI/Swagger: hard to define requests and schemas, but possible, Github does that.
So what is the best option? The usual answer: "It depends".
In the end I believe the customer experience matters the most and perhaps taking the simplest approach, like using subdomains or paths, makes adopting your API easier for your customers increasing your market-share.
Besides selecting a way to version HTTP REST APIs, the important thing to know is how and when to introduce breaking changes, the approach I like taking is the following:
-
Before adding a new major version:
- Delay v1 as much as possible, literally stay with v0 "forever", or at least make it clear to your customers breaking changes may be expected.
- Try to make your changes additive only and deprecate old ones.
-
While dealing with multiple versions. Consider a hybrid, for example Paths:
-
v1:
GET https://todo.app/v1/tasks/{taskId}
-
v1: deprecated
PUT https://todo.app/v1/tasks/{taskId}
-
v2:
PUT https://todo.app/v2/tasks/{taskId}
-
v1:
-
After:
- Encourage users to upgrade to new version by clearly indicating new features and capabilities,
- Provide a way to automatically upgrade, and
- Give a concrete deadline, plan deprecating older versions, if you can; in some cases building a bridge API to convert from one to another could be needed.
If you're looking to sink your teeth into more REST and Web Programming I recommend the following books:
20