Javascript

NestJS API Versioning Strategies

Dario Djuric
6 min read
NestJS API Versioning Strategies
This article was written over 18 months ago and may contain information that is out of date. Some content may still be relevant, but please refer to official documentation for the latest information.

Versioning is an important part of API design. It's also one of those project aspects that is not given enough thought upfront, and it often happens that it comes into play late in the game, when it's difficult to introduce breaking changes (and introducing versioning can sometimes be a breaking change). In this blog post, we will describe the various versioning strategies that you can implement in NestJS, with a special focus on the highest-matching version selection. This is a strategy that you might consider when you want to minimize the amount of changes needed to upgrade your API-level versions.

Types of versioning

In NestJS, there are four different types of versioning that can be implemented:

  • URI versioning
    • The version will be passed within the URI of the request. For example, if a request comes in to /api/v1/users, then v1 marks the version of the API.
    • This is the default in NestJS.
  • Custom header versioning
    • A custom request header will specify the version. For example, X-API-Version: 1 in a request to /api/users will request v1 version of the API.
  • Media type versioning
    • Similar to custom header versioning, a header will specify the version. Only, this time, the standard media accept header is used. For example: Accept: application/json;v=2
  • Custom versioning
    • Any aspect of the request may be used to specify the version(s). A custom function is provided to extract said version(s).
    • For example, you can implement query parameter versioning using this mechanism.

URI versioning and custom header versioning are the most common choices when implementing versioning.

Before deciding which type of versioning you want to use, it's also important to define the versioning strategy. Do you want to version on the API level? Or on the endpoint level?

If you want to go with the endpoint-versioning approach, this gives you more fine-grained control over your endpoints, without needing to reversion the entire API. The downside of this approach, is that it may get difficult to track endpoint versions. How would an API client know which version is the latest, or which endpoints are compatible with each other? There would need to be a discovery mechanism for this, or just very well maintained documentation.

API-level versioning is more common, though. With API-level versioning, every time you introduce a breaking change, you deliver a new version of the entire API, even though internally, most of the code is unchanged. There are some strategies to mitigate this, and we will focus on one in particular in this blog post. But first, let's see how we can enable versioning on our API.

Applying versions to your endpoints

The first step is to enable versioning on the NestJS application:

app.enableVersioning({
  type: VersioningType.URI,
});

With URI versioning enabled, to apply a version on an endpoint, you'd either provide the version on the @Controller decorator to apply the version to all endpoints under the controller, or you'd apply the version to a route in the controller with the @Version decorator.

In the below example, we use endpoint versioning on the findAll() method.

import { Controller, Get, Param, Version } from '@nestjs/common';

@Controller('users')
export class UsersController {
  @Get()
  @Version('1')
  findAll() {
    return 'findAll()';
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return `findOne(${id})`;
  }
}

We can invoke findAll() using curl:

➜  nestjs-versioning-strategies git:(main) ✗ curl http://localhost:3000/api/v1/users
findAll()%

How can we invoke findOne(), though? Since only findAll() is versioned, invoking findOne() needs to be without a version. When you request an endpoint without a version, NestJS will try to find so-called "version-neutral" endpoints, which are the endpoints that are not annotated with any version.

In our case, this would mean the URI we use will not contain v1 or any other version in the path:

➜  nestjs-versioning-strategies git:(main) ✗ curl http://localhost:3000/api/users/1
findOne(1)%

This happens because implicitly, NestJS considers the "version-neutral" version to be the default version if no version is requested by the API client. The default version is the version that is applied to all controllers/routes that don't have a version specified via the decorators. The versioning configuration we wrote earlier could have easily been written as:

app.enableVersioning({
  type: VersioningType.URI,
  defaultVersion: VERSION_NEUTRAL,
});

Meaning, any controllers/routes without a version (such as findAll() above), will be given the "version-neutral" version by default.

If we don't want to use version-neutral endpoints, then we can specify some other version as the default version.

app.enableVersioning({
  type: VersioningType.URI,
  defaultVersion: '1',
});

The findOne() endpoint will now return a 404, unless you call it with an explicit version. This is because we no longer have any "version-neutral" versions defined anywhere (the controllers/routes or the defaultVersion property).

➜  nestjs-versioning-strategies git:(main) ✗ curl http://localhost:3000/api/users/1
{"statusCode":404,"message":"Cannot GET /api/users/1","error":"Not Found"}%

Multiple versions

Multiple versions can be applied to a controller/route by setting the version to be an array.

import { Controller, Get, Param, Version } from '@nestjs/common';

@Controller('users')
export class UsersController {
  @Get()
  @Version(['1', '2'])
  findAll() {
    return 'findAll()';
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return `findOne(${id})`;
  }
}

Invoking /api/v1/users or /api/v2/users will both land on the same method findAll() in the controller.

Multiple versions can also be set in the defaultVersion of the versioning configuration:

app.enableVersioning({
  type: VersioningType.URI,
  defaultVersion: ['1', '2'],
});

This simply means that controllers/routes without a version decorator will be applied to both version 1 and version 2.

Selection of highest-matching version

Imagine the following scenario: You've decided to use API-level versioning, but you don't want to update all of your controllers/routes every time you increase a version of the API. You only want to do it on those that had breaking changes. Other controllers/routes should remain at whatever version they are currently.

Currently, in NestJS, there is no way of accomplishing this with just a configuration option. But fortunately, the versioning config allows you to define a custom version extractor. A version extractor is simply a function that will tell NestJS which versions the client is requesting, in order of preference. For example, if the version extractor returns an array such as ['3', '2', '1']. This means the client is requesting version 3, or version 2 if 3 is not available, or version 1 if neither 2 nor 3 is available.

This kind of highest-matching version selection does have a caveat, though. It does not reliably work with the Express server, so we need to switch to the Fastify server instead. Fortunately, that is easy in NestJS. Install the Fastify adapter first:

npm i --save @nestjs/platform-fastify

Next, provide the FastifyAdapter to the NestFactory:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { VersioningType } from '@nestjs/common';
import { FastifyAdapter } from '@nestjs/platform-fastify';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, new FastifyAdapter());
  app.setGlobalPrefix('api');
  app.enableVersioning({
    type: VersioningType.URI,
    defaultVersion: '1',
  });
  await app.listen(3000);
}
bootstrap();

And that's it. Now we can proceed onto writing the version extractor:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { VersioningType } from '@nestjs/common';
import { FastifyAdapter } from '@nestjs/platform-fastify';
import { FastifyRequest } from 'fastify';

const DEFAULT_VERSION = '1';

const extractor = (request: FastifyRequest): string | string[] => {
  const requestedVersion =
    <string>request.headers['x-api-version'] ?? DEFAULT_VERSION;

  // If requested version is N, then this generates an array like: ['N', 'N-1', 'N-2', ... , '1']
  return Array.from(
    { length: parseInt(requestedVersion) },
    (_, i) => `${i + 1}`,
  ).reverse();
};

async function bootstrap() {
  const app = await NestFactory.create(AppModule, new FastifyAdapter());
  app.setGlobalPrefix('api');
  app.enableVersioning({
    type: VersioningType.CUSTOM,
    extractor,
    defaultVersion: DEFAULT_VERSION,
  });
  await app.listen(3000);
}
bootstrap();

The version extractor uses the x-api-version header to extract the requested version and then returns an array of all possible versions up to and including the requested version. The reason why we chose to use header-based versioning in this example is that it would be too complex to implement URI-based versioning using a version extractor.

First of all, the version extractor gets an instance of FastifyRequest. This instance does not provide any properties or methods for obtaining parts of the URL. You only get the URL path in the request.url property. You would need to parse this yourself if you wanted to extract a route token or a query parameter. Secondly, you would also need to handle the routing based on the version requested.

Now, if we add multiple versions to our controller, we will always be getting the highest supported version:

import { Controller, Get, Param, Version } from '@nestjs/common';

@Controller('users')
export class UsersController {
  @Get()
  @Version('2')
  findAll2() {
    return 'findAll2()';
  }

  @Get()
  @Version('1')
  findAll1() {
    return 'findAll1()';
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return `findOne(${id})`;
  }
}

Let's test this:

➜  ~ curl http://localhost:3000/api/users/1 --header "X-Api-Version: 1"
findOne(1)%
➜  ~ curl http://localhost:3000/api/users/1 --header "X-Api-Version: 2"
findOne(1)%
➜  ~ curl http://localhost:3000/api/users --header "X-Api-Version: 2"
findAll2()%
➜  ~ curl http://localhost:3000/api/users --header "X-Api-Version: 1"
findAll1()%

We have only one findOne() implementation, which doesn't have any explicit version applied. However, since the default version is 1 (as configured in the versioning config), this means that version 1 applies to the findOne() endpoint. Now, if a client requested version 2 of our API, the version extractor would tell NestJS to first try version 2 of the endpoint if exists, or to try version 1 if it doesn't exist.

Unlike findOne(), findAll1() and findAll2() have explicit versions applied: version 1 and version 2, respectively. That's why the third and the fourth calls will return the versions that were explicitly requested by the client.

Conclusion

This was an overview of the tools you have at your disposal for implementing various versioning strategies in NestJS, with a special focus on API-level versioning and highest-matching version selection. As you can see, NestJS provides a very robust way of implementing various strategies. But some come with caveats, so it is always good to know them upfront before deciding which versioning strategy to use in your project.

The entire source code for this mini-project is available on GitHub, with the code related to highest-matching version implementation being in the highest-matching-version-selection branch.

About the author

Dario Djuric

Dario Djuric

Senior Software Engineer

Dario is a full-stack engineer who has spent most of his career doing enterprise Java projects. He has always had a hidden passion for frontend, though -- and he is now able to pursue that passion at This Dot. He spends most of his free time with his two sons, but occasionally, he manages to squeeze in some casual sports activities such as jogging, cycling, and soccer.

Keep reading

View all posts →

How to host a full-stack app with AWS CloudFront and Elastic Beanstalk

You have an SPA with a NestJS back-end. What if your app is a hit? You need to be prepared to serve thousands of users? You might need to scale your API horizontally, which means you need to have more instances running behind a load balancer. ...

Balázs Tápai12 mins
NestJSAWSTypeScriptNodeJS

Setting Up TypeORM Migrations in an Nx/NestJS Project

TypeORM is a powerful Object Relational Mapping (ORM) library for TypeScript and JavaScript that serves as an easy to use interface between an application's business logic and a database, providing an abstraction layer that is not tied to a particular database vendor. TypeORM is the recommended ORM for NestJS as both are written in TypeScript, and TypeORM is one of the most mature ORM frameworks available for TypeScript and JavaScript. One of the key features of any ORM is handling database migrations, and TypeORM is no exception. A database migration is a way to keep the database schema in sync with the application's codebase. Whenever you update your codebase's persistence layer, perhaps you'll want the database schema to be updated as well, and you want a reliable way for all developers in your team to do the same with their local development databases. In this blog post, we'll take a look at how you could implement database migrations in your development workflow if you use a NestJS project. Furthermore, we'll give you some ideas of how nx can help you as well, if you use NestJS in an nx powered monorepo. Migrations Overview In a nutshell, migrations in TypeORM are TypeScript classes that implement the MigrationInterface interface. This interface has two methods: up and down, where up is used to execute the migration, and down is used to rollback the migration. Assuming that you have an entity (class representing the table) as below: If you generate a migration from this entity, it could look as follows: As can be seen by the SQL commands, the up method will create the post table, while the down method will drop it. How do we generate the migration file, though? The recommended way is through the TypeORM CLI. TypeORM CLI and TypeScript The CLI can be installed globally, by using npm i g typeorm. It can also be used without installation by utilizing the npx command: npx typeorm . The TypeORM CLI comes with several scripts that you can use, depending on the project you have, and whether the entities are in JavaScript or TypeScript, with ESM or CommonJS modules: typeorm: for JavaScript entities typeorm ts node commonjs: for TypeScript entities using CommonJS typeorm ts node esm: for TypeScript entities using ESM Many of the TypeORM CLI commands accept a data source file as a mandatory parameter. This file provides configuration for connecting to the database as well as other properties, such as the list of entities to process. The data source file should export an instance of DataSource, as shown in the below example: To use this data source, you would need to provide its path through the d argument to the TypeORM CLI. In a NestJS project using ESM, this would be: If the DataSource did not import the Post entity from another file, this would most likely succeed. However, in our case, we would get an error saying that we "cannot use import statement outside a module". The typeorm ts node esm script expects our project to be a module and any importing files need to be modules as well. To turn the Post entity file into a module, it would need to be named post.entity.mts to be treated as a module. This kind of approach is not always preferable in NestJS projects, so one alternative is to transform our DataSource configuration to JavaScript just like NestJS is transpiled to JavaScript through Webpack. The first step is the transpilation step: Once transpiled, you can then use the regular typeorm CLI to generate a migration: Both commands can be combined together in a package.json script: After the migrations are generated, you can use the migration:run command to run the generated migrations. Let's upgrade our package.json with that command: Using Tasks in Nx If your NestJS project is part of an nx monorepo, then you can utilize nx project tasks. The benefit of this is that nx will detect your tsconfig.json as well as inject any environment variables defined in the project. Assuming that your NestJS project is located in an app called api, the above npm scripts can be written as nx tasks as follows: The typeorm generate migration and typeorm run migrations tasks depend on the build migration config task, meaning that they will always transpile the data source config first, before invoking the typeorm CLI. For example, the previous CreatePost migration could be generated through the following command: Conclusion TypeORM is an amazing ORM framework, but there are a few things you should be aware of when running migrations within a big TypeScript project like NestJS. We hope we managed to give you some tips on how to best incorporate migrations in an NestJS project, with and without nx....

Dario Djuric4 mins
NxNestJS

Combining Validators and Transformers in NestJS

When building a new API, it is imperative to validate that requests towards the API conform to a predefined specification or a contract. For example, the specification may state that an input field must be a valid e mail string. Or, the specification may state that one field is optional, while another field is mandatory. Although such validation can also be performed on the client side, we should never rely on it alone. There should always be a validation mechanism on the server side as well. After all, you never know who's acting on behalf of the client. Therefore, you can never fully trust the data you receive. Popular backend frameworks usually have a very good support for validation out of the box, and NestJS, which we will cover in this blog post, is no exception. In this blog post, we will be focusing on NestJS's validation using ValidationPipe specifically on one lesser known feature which is the ability to not only validate input, but transform it beforehand as well, thereby combining transformation and validation of data in one go. Using ValidationPipe To test this out, let's build a UsersController that supports getting a list of users, and with the option to filter by several conditions. After scaffolding our project using nest new [project name], let's define a class that will represent this collection of filters, and name it GetUsersQuery: Now, let's use it in the controller: The problem with this approach is that there is no validation performed whatsoever. Although we've defined userIds as an array of strings, and pageSize as a number, this is just compile time verification there is no runtime validation. In fact, if you execute a GET request on http://localhost:3000/users?userIds=1,2,3&pageSize=3, the query object will actually contain only string fields: There's a way to fix this in NestJS. First, let's install the dependencies needed for using data transformation and validation in NestJS: As their names would suggest, the class validator package brings support for validating data, while the class transformer package brings support for transforming data. Each package adds some decorators of their own to aid you in this. For example, the class validator package has the @IsNumber() decorator to perform runtime validation that a field is a valid number, while the class transformer package has the @Type() decorator to perform runtime transformation from one type to another. Having that in mind, let's decorate our GetUsersQuery a bit: This is not enough, though. To utilize the class validator decorators, we need to use the ValidationPipe. Additionally, to utilize the class transformer decorators, we need to use ValidationPipe with its transform: true flag: Here's what happens in the background. As said earlier, by default, every path parameter and query parameter comes over the network as a string. We could convert these values to their JavaScript primitives in the controller (array of strings and a number, respectively), or we can use the transform: true property of the ValidationPipe to do this automatically. NestJS does need some guidance on how to do it, though. That's where class transformer decorators come in. Internally, NestJS will use Class Transformer's plainToClass method to convert the above object to an instance of the GetUsersQuery class, using the Class Transformer decorators to transform the data along the way. After this, our object becomes: Now, Class Validator comes in, using its annotations to validate that the data comes in as expected. Why is Class Validator needed if we already transformed the data beforehand? Well, Class Transformer will not throw any errors if it failed to transform the data. This means that, if you provided a string like "testPageSize" to the pageSize query parameter, our query object will actually come in as: And this is where Class Validator will kick in and raise an error that pageSize is not a proper number: Other transformation options The @Type and @Transform decorators give us all kinds of options for transforming data. For example, strings can be converted to dates and then validated using the following combination of decorators: We can do the same for booleans: If we wanted to define advanced transformation rules, we can do so through an anonymous function passed to the @Transform decorator. With the following transformation, we can also accept isActive=1 in addition to isActive=true, and it will properly get converted to a boolean value: Conclusion This was an overview of the various options you have at your disposal when validating and transforming data. As you can see, NestJS gives you many options to declaratively define your validation and transformation rules, which will be enforced by ValidationPipe. This allows you to focus on your business logic in controllers and services, while being assured that the controller inputs have been properly validated. You'll find the source code for this blog post's project on our GitHub....

Dario Djuric4 mins
NestJS

Introduction to RESTful APIs with NestJS

Introduction on RESTful API with NestJS, covering topics such as module organization, service and controller implementation, testing with Insomnia, logging, Swagger documentation, and exception handling....

Steven Spadotto13 mins
NestJSJavaScriptNodeJS