NestJS tip: how to attach decorators to all controllers at once

micalevisk

Micael Levi L. C.

Posted on September 29, 2024

NestJS tip: how to attach decorators to all controllers at once

What

In the official NestJS integration with OpenAPI (formerly Swagger) @nestjs/swagger we have a bunch of @Api* decorator factories such as @ApiBearerAuth().

Due to how TypeScript decorators works, you cannot bind those class decorators globally -- ie., for all the controllers that your entry module has discovered -- at once.

But using the built-in provider DiscoveryService (from @nestjs/core package exported by the DiscoveryModule NestJS module) we can do that!

DISCLAIMER: I'm not saying that this is good. I don't like the ideia of not having those decorators close to the controller.

How

For this demo, our controller will be as simple as this:

  • app.controller.ts
import { Controller, Get } from '@nestjs/common'

@Controller()
export class AppController { // No Api* decorators here!
  @Get()
  hello() {
    return 'Hello, World!'
  }
}
Enter fullscreen mode Exit fullscreen mode

Our entry module would look like this:

  • app.module.ts
import { Module } from '@nestjs/common'
import { DiscoveryModule } from '@nestjs/core'
import { AppController } from './app.controller'

@Module({
  imports: [
    DiscoveryModule, // We'll use DiscoveryService later
  ],
  controllers: [
    AppController,
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

And our entry file would look like this:

  • main.ts
import { DiscoveryService, NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { ApiBearerAuth, DocumentBuilder, SwaggerModule } from '@nestjs/swagger'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)

  const makeDocument = () => {
    const discoveryService = app.get(DiscoveryService)
    const controllers = discoveryService.getControllers()
    for (const controller of controllers) {
      // All the ApiX decorators that you want to bind globally
      ApiBearerAuth()(controller.metatype) // (LA)
    }

    const config = new DocumentBuilder()
      .setTitle('Cats example')
      .build()
    return SwaggerModule.createDocument(app, config) // (LB)
  }

  SwaggerModule.setup('/api', app, makeDocument) // (LC)

  await app.listen(3000) // (LD)
}
bootstrap()
Enter fullscreen mode Exit fullscreen mode

Why this works

The SwaggerModule.createDocument is responsible to compose the OpenAPI spec. and will be invoked by SwaggerModule.setup when this object is needed. In our case we are supplying a factory that returns the document, not the document itself. This is important!

Also, we must call SwaggerModule.setup before app.listen/app.init because this one is responsible to add a new route (the /api in this example) that exposes the OpenAPI spec. and the Swagger UI.

In order to enhance all the controllers with @Api* decorators, we must run the code at (LA) line before (LB).

Due to the lazy loading mode of SwaggerModule.setup when calling our makeDocument factory, everything will work as we want:

  • SwaggerModule.setup is being called before app.listen
  • SwaggerModule.createDocument is being called after our custom enhancement of all the controllers found from AppModule (including child modules); thus, after line (LA)

At http://localhost:3000/api we could see this:

demo swagger UI result

To be fair, we don't need to rely on the lazy loading mode of SwaggerModule.setup as long as we call (LA) before SwaggerModule.createDocument, but I prefer this approach to avoid impacting the bootstrapping time.


Of course that you can find alternatives approaches such as relying on the on module init lifecycle hook instead of having to use the app inside the makeDocument factory function.

💖 💪 🙅 🚩
micalevisk
Micael Levi L. C.

Posted on September 29, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related