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.setup
is being called beforeapp.listen
-
SwaggerModule.createDocument
is 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 (1)
Thanks for the tip!
It looks the DiscoveryService is becoming more popular. I am also writing an article about another use case!