Swagger y wiring del módulo
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 {}
providers: todos los use cases de la feature, y los services que use.exports: solo lo que otro módulo consume de verdad. Aquí únicamenteCreatePetTagUseCase, porquePetsModulelo necesita al crear una mascota. Exportarlo todo “por si acaso” convierte el módulo en una superficie pública que nadie revisa.- Varios controllers en un mismo módulo es normal cuando cubren la misma feature — aquí uno autenticado y otro público.
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
- Un
.docs.tspor controller, sin anotación de operación inline -
@ApiTags/@ApiBearerAuthen la clase, no en los métodos - Si el área se reparte en varios controllers, la autorización en un decorador compartido
-
exportsreducido a lo que otro módulo importa de verdad - El módulo registrado en
app.module.ts