Best Practices, Tips & Tricks
API Versioning is a hot topic today, and while the number of APIs has started to increase, there hasn’t been a good formalization of related best practices.
In this article, we will focus mostly on internal APIs, as this use case grants us the highest level of control and possible alternatives.
When do we need to consider it?
Currently Agile methodologies are used to improve time to market, increase customer satisfaction and decrease risks related to development. Projects developed in this way will usually deliver a minimum viable product first (MVP), then follow up with new features based on customer feedback and evolving business needs.
If you have complete control over the clients to your API, versioning doesn’t need to be taken into consideration. You don’t have control when you’re developing externally accessible APIs (public, integration for external systems) or you have mobile app clients (although you can forbid the user from using an old version of the application, and asking him to update, you can’t force update it without his/her consent, yet)
How do we manage it?
You usually have one common database for all your versions. If you don’t, then it’s just like having multiple separate APIs, so there’s no need to do anything.
There are normally two types of endpoints one could expose in an API:
- Data endpoints, which update data (CRUD operations)
- Action endpoints, which trigger the business flows of your application
There are also a number of ways in which these endpoints may need modification. For data endpoints, one could add, change or remove fields from the data model. Similarly, for action endpoints, a business flow can be added, changed or removed.
Two possible solutions to handling such changes in a continuously evolving app are backwards compatibility and running multiple versions of your API at the same time
Planning for backwards compatibility
If it’s applicable, backwards compatibility works best. This way, you have one version of your API which services all your clients.
It’s usually easier to apply it to the data endpoints of your application.
When adding a new column in the database, you can usually also provide a default value for it, and manage that default value in your API.
When removing a column from the database, one could simply ignore it from the API input and move forward.
Adding totally new data endpoints works just fine.
However, when an action endpoint changes, things are more tricky. If all that changes is contained within the business logic, then the API is safe. If you need to add to your input model, then that’s a breaking change. The same will happen if you need to remove something from your output model. These types of changes can’t be handled through backwards compatibility.
The advantages of using backwards compatibility is less versions to manage, less time spent on QA, less time spent on the initial development. The disadvantage is that you are increasing the complexity of your code, and, without proper coding documentation, that complexity will remain even after you have no clients left for your old version. This makes the maintenance and evolution of the codebase harder, future development will take longer, and most opportunities to refactor will be discouraged.
Managing multiple versions
For the scope of this article, it’s not important how the clients differentiate between which version to call. There are multiple available approaches, and the best one can be chosen based on the project context (different URLs, managed by an API gateway through complex rules, etc).
However, we are interested in how multiple versions can live together
In relation to data endpoints, deletions of properties from the data model is possibly the only instance which can cause problems.
For action endpoints, anything that changes the input or output is a breaking change. Examples are: adding a new required input parameter, deleting a previously mandatory field from the output, changing the structure of an input parameters (returning a list of ids instead of one id, maps instead of lists, etc)
If this is the case, it’s enough to deploy your versions separately. When the database model changes as well, that means the older versions need to be patched to work with it. So, when getting ready to release, you’re not releasing one new version, you’re actually releasing one plus all the previous versions, patched.
The advantage of this approach is that your code is cleaner, your business logic clearer and your tests simpler. There’s no mix between various versions, making everything easier to refactor and to maintain.
The disadvantage is that the time you spend preparing for a release increases. QA will take more because they will have to run specific tests to confirm multiple versions are working together, in addition to regressions tests for all of them. Your code management also becomes brittle, because you end up with patches in your previous release branches which won’t ever make it to your latest release branch. All this extra complexity naturally limits the number of strictly different versions which you can manage.
Tips and tricks
Improving the API design
We can consider designing our API based on the type of clients which access it. API for mobile clients should be extracted in separate modules, as we have less control over when they update. Obviously, there will never be a clean split with the rest of the code, but the advantages exist either way:
- You become aware of and isolate the different sections in your API
- You can keep multiple versions only where you need
- The quantity and complexity of extra testing needed by QA decreases significantly
Splitting into multiple services
Transitioning to a microservice oriented architecture also helps. Each service is smaller than a full fledged application, therefore easier to manage. Changes to existing features are isolated to the services they impact and their clients (and usually it’s not all of them). However, how all your services integrate will need some extra work.
The best candidates for extraction to a separate service are those modules which are rarely changed. Since they’re rarely changed, you won’t need to bother versioning them. You will also decrease the load on QA, with less regression tests and integration tests to run on each occasion. It’s an obvious trade-off which needs to be made, with no short-term gain, but which, in the long-term, will keep your system modular, easier to maintain and easier to build upon.
If not such modules can be identified, then it might be better to stay in a monolithic architecture, as increasing the number of versions you need to keep track of will only increase your complexity and insert room for error.
In our example, I’ve considered that we can break the code into 2 separate services, Service 1 and Service 2. Clients make requests to both of them, through a gateway (not displayed).
You also need to apply a consistent process for the deprecation of older version. This means:
- Communicate changes to your users
- Allow them adequate time to update
- Provide support where needed
Communicating changes to your users can be any number of things, from adding a banner in your mobile app to adding an announcement on your website.
Deciding on what adequate means for your users is also crucial. Having a minimum amount of time for which a version will be available (e.g.: 3 months, 6 months, etc.), along with a minimum amount of time since you deprecate a version until you delete it, will make the evolution of your API predictable and will give a feeling of stability and trustworthiness.
There’s also a question of providing support to your users. This may be something as simple of updating your API documentation, but could also mean the development of training materials or training plans to your users.
Although API versioning is essentially a technical challenge, in order to tackle it successfully in your project, everyone needs to be aware of it. Identifying possible breaking changes as early as possible and planning for them means that all the team members will be involved, from project management, business analysis and design, to development, QA and devops.
Thanks to Iulian and Vlad for providing their feedback during the writing of this article.