DEV Community

Marvin Rocha
Marvin Rocha

Posted on

NestJS controllers drowning in Swagger decorators? I built a small fix!

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
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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' }),
    ],
  },
});
Enter fullscreen mode Exit fullscreen mode

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)