DEV Community

ChunTing Wu
ChunTing Wu

Posted on

Implementing API Versioning

For a public API to be called by a user, it is common practice to use versioning to control the impact. For example, when a user calls an API and expects certain results, a change in the API's interface or behavior can cause unpredictable risks for the caller. Therefore, in practice, the original API will be tuned to version 1, and the modified API to version 2.

Users can be sure that calling the original version 1 API will not cause any problems, and the service providing the API can continue to iterate on the functionality, both sides of the development cycle can be independent of each other. Of course, maintaining two sets of APIs increases the maintenance effort for the service provider, so APIs have a lifecycle and don't live forever.

In the case of the AWS services, the APIs called by the SDK have a version number (named by date), so it is recommended to specify the version of the used API in the production environment.

The following is an example of the AWS Python SDK, boto3, which according to the official document, api_versions should be specified in AWS configure. The version change of each service is available in this channel.

https://awsapichanges.info/

In fact, there are many different ways of implementing API versioning, and one interesting example is Shopify, according to Shopify's official document, we know that Shopify releases one version per quarter and maintains only four versions at a time, i.e. one year.

Versions are date-named and embedded directly in the URL, and those out-of-date versions fallback to the oldest version in available support.

In other words, suppose the current four versions are as follows.

  • 2023-07
  • 2023-04
  • 2023-01
  • 2022-10

Then the call to 2022-07 would use the version 2022-10.

This is an interesting way of implementation, as the client has one year to make changes, and if it doesn't, it will still work, but with unexpected results, instead of just crashing.

How to implement such a versioning mechanism? This article provides a possible approach.

Implementation approach

The full source is in the following repo.

https://github.com/wirelessr/versioning

The overall implementation architecture is as follows.

Image description

All three services implement two URIs: /hello and /hi, which just print the URI with a version number.

For users, to call the corresponding service, they just put a prefix in front of the URI, e.g. curl http://localhost/v2/hello would print out

hello v2

In addition, calling a non-white-listed (v1, v2 and v3) version will fallback to v1, e.g., curl http://localhost/v4/hello will print

hello v1

The core of this experiment is nginx on the gateway.

http {
    server {
        listen 80;

        location ^~ /v1/ {
            rewrite /v1/(.*) /$1 break;
            proxy_pass http://web_v1;
        }

        location ^~ /v2/ {
            rewrite /v2/(.*) /$1 break;
            proxy_pass http://web_v2;
        }

        location ^~ /v3/ {
            rewrite /v3/(.*) /$1 break;
            proxy_pass http://web_v3;
        }

        location ~ /v(\d+)/ {
            rewrite /v(\d+)/(.*) /$2 break;
            proxy_pass http://web_v1;
        }

    }
}
Enter fullscreen mode Exit fullscreen mode

If the location matches the previous 3 rules, then rewrite the original URI, remove the prefix and redirect to the corresponding service, if it doesn't match the previous rules but matches the version specification (v plus integer), then redirect to the v1 service anyway.

By using nginx regex, we can make the version match the corresponding service and implement the extra fallback mechanism.

Conclusion

Actually, there is another approach to provide various versions of the API in the original service, for instance, opening several endpoints directly in the API service as follows.

  • /v1/hello
  • /v2/hello
  • /v3/hello

Creating three versions of the API directly instead of adding a prefix to the API through the gateway is not recommended. If you don't physically isolate them, it adds a lot of development overhead, as in the following real-world example.

Image description

When we need to modify the behavior of the common lib, it will inevitably affect v1, which makes it difficult to iterate on the functionality, and also introduces risk to the user.

Therefore, isolation at the physical level is more controllable than isolation at the logical level. This article provides a possible approach, but there are many other implementations that can achieve the same result, so feel free to share them with me.

Top comments (0)