Multiple concurrent api versions with Node and NestJS
Introduction:
I have worked on a project with a specific need to keep multiple versions of the same API up and running. I had to associate documentation with a set of API services to ensure that this orchestration of services was aligned and available simultaneously in the infrastructure. Clients would access the API in different versions, following the corresponding documentation, passing different parameters, and even receiving different types of responses depending on the client-side implemented version. It was a major challenge due to the complex architecture and specific requirements. To tackle this, I utilized the infrastructure of Google Cloud Platform (GCP) integrated with source code on GitHub, using GitHub Actions as the CI/CD service. In this article, I will explain how I addressed this demand and discuss the technical details and challenges overcome.
Architecture: 🏗️
In this architecture, I used Node.js with NestJS for building microservices. The APIs communicated with each other and were divided into service layers aligned with the business domain. Some layers had internal communication, while others had external communication. Additionally, there was a dashboard panel for administration control of generated tokens with access levels for each API service.
🔍Versioning Logic:
To achieve versioning in this project, I used Postman documentation associated with each new API version. GitHub Actions were employed to run automated tests, build, deploy, and generate documentation automatically. The services were deployed to Google Cloud Run, with a tag created for each version. I followed the logic of semantic versioning with three levels: low-level for bug fixes, medium-level for adding new features without breaking compatibility, and high-level for breaking changes and compatibility. For example, a version tag could be v0.1.0, representing the three levels separated by dots, thus following the normative rfc2119, and guidelines of the author Tom Preston-Werner.
Semantic Versioning is a versioning scheme for software that aims to convey meaning about the underlying code changes. It consists of three levels: MAJOR.MINOR.PATCH. Incrementing the:
• MAJOR version indicates incompatible changes and requires updates on the client side.
• MINOR version signifies backward-compatible additions or improvements.
• PATCH version denotes backward-compatible bug fixes.
Infrastructure on Google Cloud Platform: 🌩️
For infrastructure, I utilized GCP services. I used Firestore for data storage, as it allowed me to have multiple entity records with slightly different data structures, enabling versioning for the API’s data layer. To support the API services, I used Cloud Run as the service orchestrator for my NestJS applications. The services were exposed using API Gateway, which handled the load balancing and endpoint mapping to the respective services.
Google Cloud Run is a fully managed serverless execution environment that enables running stateless containers. It automatically scales the container instances based on incoming request traffic and provides a simple way to deploy and manage containerized applications.
Example code for deploying a service to Cloud Run using the Google Cloud SDK:
gcloud run deploy my-service --image gcr.io/my-project/my-service --platform managed
🚀CI/CD:
The pipeline scripts were created using YAML files and built on top of GitHub Actions deployment resources. I implemented automated testing scripts in a pipeline job using Jest as the testing tool and libraries such as Faker for generating test data. For the build script, I containerized the applications using Docker for consistency, decoupling, and control of the runtime environment. The artifacts were stored in the GCP Registry, followed by the generation of the versioning tag. After implementing the logic for tag generation based on the semantic versioning approach mentioned earlier, the next step in the pipeline was generating documentation for Postman. The documentation version was automatically published and aligned with the generated API version. Finally, the deployment was performed to Cloud Run, and the API routes were updated to include the version number. When clients hit the API, they are instructed to include the desired version number in the route URL. If no version is specified, they are directed to the latest version of the API.
GitHub Actions is a CI/CD platform provided by GitHub. It allows automating workflows, including building, testing, and deploying applications. Workflows are defined using YAML files and can be triggered by various events, such as pushes to the repository or pull requests.
Example code for a GitHub Actions workflow:
name: CI/CD Pipeline
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build Docker image
run: docker build -t my-api .
- name: Push Docker image
run: docker push my-registry/my-api
# ... Continue with other steps
Implementation Details in NodeJS:
In NestJS, I implemented API versioning using the approach of route prefixes. This choice allows me to have more precise control over the versioning of resources in the URI. I started by creating separate modules for each API version. For example, I created the v1 module and the v2 module, where I could define the controllers, service providers, and other components specific to each version. Within each module, I added the corresponding routes for that specific version. For example, in the v1 module, I created a controller that defines the /users route to handle user-related operations in the first version of the API. In the v2 module, I created a separate controller for the /users route, with different logic for the second version of the API. To add route prefixes for each version, I took advantage of the routing scope functionality of NestJS. This allowed me to group the routes under a specific URI prefix. When starting the NestJS application, the framework automatically handles the routes based on the defined prefixes. This means that requests to /v1/users will be directed to the controller of the first version, while requests to /v2/users will be directed to the controller of the second version.
Conclusion:
In this article, we explored the techniques of maintaining multiple versions of an API using semantic versioning and serverless architecture on the Google Cloud Platform (GCP). We discussed the challenges faced and the technical details of the implementation. By leveraging Node.js, NestJS, and GCP services, we were able to create a robust and scalable architecture for our microservices.
One of the key aspects of our solution was the implementation of semantic versioning, which provided a clear and intuitive way to manage API versions. By following the semantic versioning scheme, we were able to communicate the impact of changes to our clients and ensure compatibility between different versions.
Using GCP services such as Firestore, Cloud Run, and API Gateway, we built a resilient infrastructure capable of handling the demands of multiple versions of the API. The combination of these services allowed us to store data with version-specific structures, deploy and orchestrate our services, and efficiently route incoming requests.
With the help of GitHub Actions, our CI/CD pipeline became streamlined and automated. We could easily run tests, build Docker containers, generate documentation, and deploy our services to Cloud Run. This automated approach increased our development efficiency and helped maintain the consistency and quality of our codebase.
By integrating documentation generation into the pipeline, we ensured that the API documentation was always up to date and aligned with the corresponding API version. This made it easier for clients to understand and interact with the API, promoting a smooth developer experience.
In conclusion, the combination of semantic versioning, serverless architecture, and GCP services allowed us to effectively manage multiple versions of an API. By leveraging the power of Node.js, NestJS, and GitHub Actions, we achieved a high level of maintainability and scalability. As technology continues to evolve, these techniques will be invaluable for organizations seeking to provide backward compatibility, introduce new features, and manage complex API ecosystems.
Top comments (0)