I want to share something I've been thinking about for a while while working with NestJS, and a small library I put together to deal with it. Not a revolutionary idea, just something that bothered me enough to actually do something about it.
The thing that kept bothering me
Every time I documented an endpoint, my controller went from something readable to something like this:
@ApiTags('users')
@Controller('users')
export class UsersController {
@Post()
@ApiOperation({ summary: 'Create a user' })
@ApiBody({ type: CreateUserDto })
@ApiResponse({ status: 201, type: UserEntity })
@ApiResponse({ status: 400, description: 'Invalid input' })
create(@Body() dto: CreateUserDto): Promise<UserEntity> {
return this.usersService.create(dto);
}
// ... imagine 4 more endpoints like this
}
The actual routing logi, the part I care about, gets buried under documentation metadata. They're two different concerns sharing the same file, and it starts to feel noisy.
We already separate tests into *.spec.ts files. It felt weird that documentation was treated differently.
What I tried
I built nestjs-docfy, a small module that moves Swagger decorators to a companion file using a *.controller.docs.ts naming convention. The controller stays focused on routing, and the docs file handles everything OpenAPI-related.
users.controller.ts after
@WithDocs()
@Controller('users')
export class UsersController {
@Post()
create(@Body() dto: CreateUserDto): Promise<UserEntity> {
return this.usersService.create(dto);
}
}
users.controller.docs.ts
import { docs } from 'nestjs-docfy';
docs(UsersController, {
classDecorators: [ApiTags('users')],
methods: {
create: [
ApiOperation({ summary: 'Create a user' }),
ApiBody({ type: CreateUserDto }),
ApiResponse({ status: HttpStatus.CREATED, type: UserEntity }),
ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid input' }),
],
},
});
How it works
You add DocfyModule.forRoot() to your AppModule, that's the only required change. During NestFactory.create(), the module resolves companion files for controllers marked with @WithDocs() and applies the metadata via Reflect. Since this happens before SwaggerModule.createDocument() runs, the Swagger generator sees everything exactly as if the decorators were inline.
No changes to your bootstrap flow, no magic at runtime.
It's a small thing, but it made my controllers a lot easier to read. If your team deals with the same noise, it might be worth a look.
๐ nestjs-docfy on GitHub ยท ๐ฆ npm package
Curious if this separation makes sense in your codebase, or if there's a better way you've found to handle it.
Top comments (0)