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!'
}
}
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 {}
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()
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.setupis being called beforeapp.listen -
SwaggerModule.createDocumentis being called after our custom enhancement of all the controllers found fromAppModule(including child modules); thus, after line(LA)
At http://localhost:3000/api we could see this:
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.

Top comments (2)
If you're using Public decorator, make sure to exclude unnecessary routes from authentication.
The following code ensures that only routes without the
@Public()decorator require authentication in Swagger documentation:Make sure to place this code before SwaggerModule.setup.
Thanks for the tip!
It looks the DiscoveryService is becoming more popular. I am also writing an article about another use case!