ecosystem-wikicómo extender los templates sin romper sus convenciones
‹ todas las recipes

Swagger y wiring del módulo

template-api
último cambio 2026-09-25

Las dos piezas mecánicas que cierran una feature. Poco que decidir, bastante que respetar.

Swagger vive fuera del controller

23 controllers, 23 ficheros .docs.ts. Cero @ApiOperation inline. Un controller anotado es ilegible: la firma del método queda enterrada bajo veinte líneas de decoradores.

// presentation/pet-tags/pet-tag.docs.ts
export const ApiRegenerate = () =>
  applyDecorators(
    ApiOperation({
      summary: "Issue a new tag for a pet and revoke the previous one",
      description:
        "The old code stops resolving publicly but stays in the database, still linked to the pet.",
    }),
    ApiOkResponse({ type: PetTagResponseDto }),
  );
// pet-tag.controller.ts — se lee de un vistazo
@ApiRegenerate()
@Post("regenerate")
async regenerate(
  @CurrentUser() user: User,
  @Param("petId") petId: string,
  @Body() dto: RegeneratePetTagDto,
): Promise<PetTagResponseDto> {

Convención: una función Api<Operación>() exportada por endpoint, nombrada como el método.

Lo que sí se queda en el controller son los decoradores de clase: @ApiTags, @ApiBearerAuth, @Controller. Esos describen el controller entero, no una operación.

Autorización repetida → un decorador

Cuando un área se reparte en varios controllers, agrupar guard y rol en un decorador propio no es cosmético:

export const SuperadminRoute = () =>
  applyDecorators(
    ApiTags("Superadmin"),
    ApiBearerAuth(),
    UseGuards(RolesGuard),
    Roles(Role.SUPERADMIN),
  );

El motivo está escrito en el fichero: repetir las cuatro líneas a mano en cada controller nuevo “abre la puerta a que un controller nuevo se olvide de la parte de autorización y quede accesible a cualquier usuario autenticado”. Un olvido de una línea deja una ruta de superadmin abierta.

El ApiTags viaja dentro a propósito, para que los controllers repartidos sigan saliendo agrupados bajo una sola sección en Swagger.

El módulo

Vive en presentation/, aunque declare use cases de application/:

@Module({
  controllers: [PetTagController, PublicPetProfileController],
  providers: [
    PetAccessService,
    CreatePetTagUseCase,
    GetOrCreatePetTagUseCase,
    RegeneratePetTagUseCase,
    GetPublicPetProfileUseCase,
  ],
  exports: [CreatePetTagUseCase],
})
export class PetTagsModule {}

Los repositorios y los adaptadores no se declaran aquí: viven en módulos globales (persistence.module.ts, security.module.ts, storage.module.ts…) que atan cada símbolo a su implementación una sola vez.

Checklist de cierre