A change to an API several clients depend on has one unpleasant property: it cannot be withdrawn from other people's applications. You remove a field nobody - you believe - uses, and that same afternoon you learn that an integrator at the other end of the country was using it.
Rome managed this through eras. The law of the Republic did not vanish when the Empire came - it went on governing matters begun earlier, while the new rules ran alongside. API versioning is the same: the old version lives as long as somebody uses it, and changes go into the new one.
NestJS knows three ways of stating a version:
/v1/legiones. The most commonly used, because it shows in the logs, in the browser and in bookmarks.X-Version: 1. The address stays clean, but the version cannot be seen without inspecting the request.Content-Type header. The most faithful to the spirit of HTTP and the rarest in practice.With URI Versioning at version one the address looks like this:
. Not /v1/legiones
/legiones?version=1 - that would be a query parameter, not a version. Not /legiones/v1 - there v1 would look like a resource identifier. And not /legiones with an X-Version header - that is already a different strategy.Versioning is switched on once, in
main.ts:1async function bootstrap() {
2 const app = await NestFactory.create(AppModule);
3
4 app.enableVersioning({
5 type: VersioningType.URI,
6 defaultVersion: '1',
7 });
8
9 await app.listen(3000);
10}The notation has four parts in a fixed order:
opens the configuration, app.enableVersioning({
chooses the strategy, type: VersioningType.URI,
sets the version for controllers that do not state one, and defaultVersion: '1'
closes it.});
defaultVersion is worth setting straight away. Without it, every controller lacking an explicit version stops answering - and when you switch versioning on in an existing project that usually means all of them at once.A controller declares its version in the same decorator that gives its path:
1@Controller({
2 path: 'legiones', version: '1' })
3export class LegionV1Controller {}The written order is fixed:
opens the configuration object, @Controller({
supplies the path and the version, and path: 'legiones', version: '1' })
declares the class itself.export class LegionV1Controller {}
Note that
@Controller('legiones') with a string becomes @Controller({ path, version }) with an object - the same function, in the variant that accepts more options. Individual methods can be versioned separately with the @Version('2') decorator, when only one of them has changed.The class name -
LegionV1Controller - means nothing to the routing; the version is settled by the version field alone. It is still worth naming it after the version, because in a project with two eras, two LegionController classes in different files soon become a source of mistakes.One Swagger document with the versions mixed together is less useful than two separate ones. The process has four steps, in this order:
app.enableVersioning() - the application must first be able to tell versions apart at all.DocumentBuilder for each version - a separate configuration with its own title and number.SwaggerModule.createDocument with the include option - building a document from the chosen modules only.SwaggerModule.setup for each version - serving each document at its own address.1const configV1 = new DocumentBuilder()
2 .setTitle('Legion API v1')
3 .setDescription('The first era of the Empire')
4 .setVersion('1.0')
5 .setContact('Chancery', 'https://imperium.rome', 'chancery@imperium.rome')
6 .setLicense('MIT', 'https://opensource.org/licenses/MIT')
7 .addServer('https://api.imperium.rome')
8 .addTag('Legions')
9 .addBearerAuth(undefined, 'JWT-auth')
10 .build();
11
12const documentV1 = SwaggerModule.createDocument(app, configV1, {
13 include: [LegionV1Module],
14});
15
16SwaggerModule.setup('api/v1', app, documentV1);A
DocumentBuilder is assembled from a chain of methods, each adding one element of the description: setTitle, setDescription and setVersion are the basics, setContact says whom to write to, setLicense gives the licence, addServer the address at which the API actually runs, addTag declares a group of endpoints, and addBearerAuth switches on the token field.The
option includes only selected modules in the documentation. It does not add CSS files to Swagger UI, does not import external OpenAPI specifications, and does not enable additional decorators. It is what makes the include
v1 document contain only the first era's endpoints - without it both documents would be identical and would show the whole API.Finally
SwaggerModule.setup('api/v1', ...) serves the document at /api/v1. The second version is built the same way: a configV2 with its own title and number, include: [LegionV2Module], and setup('api/v2', ...) at the end.Version two does not annul version one overnight. Endpoints that are to disappear are first marked as deprecated:
1@ApiOperation({
2 summary: 'List of legions (old version)',
3 deprecated: true,
4})
5@Get()
6findAllLegacy() {
7 return this.legionService.findAll();
8}You mark an endpoint as deprecated with the
property in deprecated: true
@ApiOperation. Not removed, not obsolete, not disabled - those three names sound sensible, but they do not exist in OpenAPI; the specification knows only deprecated.In Swagger UI such an endpoint is struck through and carries a warning, but it still works. That is the point: the marking is an announcement, not a switch-off. It gives integrators time to move across before the endpoint truly disappears - and without that transition period versioning is pointless, since the change breaks other people's applications anyway.
The law of the Republic holds while matters begun under the Republic remain, @name:
X-Version), Media Type (in Content-Type),/v1/legiones - not /legiones?version=1, not /legiones/v1,main.ts: app.enableVersioning({ → type: VersioningType.URI, → defaultVersion: '1' → });,defaultVersion protects controllers that state no version,@Controller({ → path: 'legiones', version: '1' }) → export class LegionV1Controller {},app.enableVersioning() → a DocumentBuilder per version → createDocument with include → SwaggerModule.setup per version,DocumentBuilder: setTitle, setDescription, setVersion, setContact, setLicense, addServer, addTag, addBearerAuth,include option includes only selected modules - it adds no CSS, imports no external specifications, enables no decorators,deprecated: true in @ApiOperation marks an endpoint as deprecated - not removed, not obsolete, not disabled; the endpoint still works.In the next lesson we gather the whole module into one project - the complete documentation of the Imperium API. For now remember: versioning is not there to let you make changes. It is there so that a change does not ruin somebody's day.