Julio César

Desarrollador de software, con experiencia en desarrollo web y móvil. Apasionado por la tecnología y la innovación.

Documentación automática con Swagger en NestJS

Cuando estamos construyendo un servicio web, una documentación de alta calidad es tan importante como la calidad del código que escribimos. Finalmente la documentación terminamos consumiendo nosotros mismos en el futuro o los desarrolladores de otros servicios o clientes que vayan a consumir nuestros recursos.

En esta entrada vamos a ver todo lo que necesitamos para generar automáticamente una documentación de alta calidad con Swagger, formalmente conocido ahora como la especificación OpenAPI.

Contenido


Instalación de dependencias

El primer paso que debemos ejecutar es la instalación de el paquete swagger en nuestro proyecto, si usamos npm ejecutamos la siguiente instrucción:

npm install --save @nestjs/swagger

En caso de usar yarn ejecutamos la siguiente acción:

yarn add @nestjs/swagger

Configuración del proyecto

Una vez instalada el paquete swagger vamos a ir al archivo main.ts y agregamos la siguiente configuración:

import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('Blog API')
    .setDescription('The blog API description')
    .setVersion('1.0')
    .addTag('blog')
    .build();
  const documentFactory = () => SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, documentFactory, {
    jsonDocumentUrl: '/swagger-json',
  });

  ...
}
await bootstrap();

En el fragmento anterior agregamos el módulo SwaggerModule a nuestra aplicación y al mismo tiempo creamos un documento de OpenAPI que puede ser accedido en la ruta docs. Adicionalmente, configuramos la ruta swagger-json para exponer la documentación generada por Swagger en formato JSON para que pueda ser consumido por modelos de IA o herramientas automatizadas.

Documentación de DTO

Para documentar los atributos de un DTO tenemos algunos decoradores muy útiles, como, por ejemplo:

Estos decoradores vienen del paquete @nestjs/swagger. Como el nombre indica uno se usa cuando la propiedad es requerida y el otro cuando es opcional.

Estos decoradores reciben un objeto de configuración que puede contener los siguientes atributos:

Entre otras posibles configuraciones, es posible profundizar más en estos decoradores en la documentación oficial de NestJS para Swagger.

Para los DTO de tipo Update, aquellos que se derivan de DTOs definidos completamente a los que se aplica el mapped type PartialType, debemos mantenerlos como fueron definidos, pero ahora el paquete se debe importar desde @nestjs/swagger que se encarga de agregar el decorador @ApiPropertyOptional() internamente.

Un ejemplo de un UpdateDTO se muestra a continuación:

import { PartialType } from "@nestjs/swagger";
import { CreatePostDto } from "./create-post.dto";

export class UpdatePostDto extends PartialType(CreatePostDto) {}

Documentación de endpoints

Como ya sabemos los endpoints son gestionados desde los controladores, por lo tanto, vamos a ver los decoradores y parámetros que debemos usar en un controller con el fin de generar una buena documentación.

Descripción del endpoint

Para un endpoint podemos hacer uso del decorador @ApiOperation() y pasar los siguientes atributos en el objeto de configuración:

Un ejemplo de los anterior es:

 @ApiOperation({
    summary: 'Get all users',
    description: 'Retrieve a list of all users registered in the system',
    deprecated: true
  })
  @Get()
  getUsers() {
    return this.userService.getUsers();
  }

Describir path params

Como vimos en una entrada anterior dedicada a los llamados query params y path params, aprendimos que los path params corresponden a segmentos variables o parametrizables de una ruta, son valores variables normalmente utilizados para identificar un recurso específico dentro de un conjunto.

Documentar los path params es de suma importancia para mejorar la experiencia de desarrollo de quienes consumen nuestra API. Podemos documentar un path param utilizando el decorador @ApiParam(). Observemos el siguiente ejemplo:

@Controller('products')
export class ProductsController {

@Get(':id')
@ApiParam({
  name: 'id',
  type: String,
  description: 'The unique identifier of the product',
  example: 'prod_95x82103',
})
findOne(@Param('id') id: string) {
  return `This action returns product #${id}`;
}

Describir query params

Los query params corresponden a valores que se pasan en la URL después del símbolo ? y que vienen dados en forma de key=value separados por un símbolo &. Los query params son el mecanismo utilizado para filtrar, agrupar u ordenar recursos en un conjunto.

Un ejemplo típico de una URL con query params puede ser un endpoint retorna una lista de vehículos pero puede filtrar por marca y además tiene paginación y retorna máximo 5 elementos:

https://example.com/cars?brand=chevrolet&limit=5

Podemos documentar los query params de dos formas, la primera es individualmente, y está bien cuando son uno o dos query params máximo, como vemos a continuación:

@Get('/cars')
@ApiOperation({ summary: 'Return a list of cars with pagination' })
@ApiQuery({
  name: 'limit',
  type: Number,
  required: false,
  description: 'Number of items to return per page',
  example: 10,
})
@ApiQuery({
  name: 'brand',
  type: String,
  required: false,
  description: 'Filter cars by brand name',
})
findAll(@Query('limit') limit?: number, @Query('brand') brand?: string) {
  return `Returns cars filtered by "${brand}", limited to ${limit} items.`;
}

Por otra parte, cuando tenemos varios query params que pueden entrar en nuestro servicio, lo más adecuado es crear un DTO, es decir, una clase que en sus atributos describe cada uno de los query params y además los anota con los decoradores @ApiProperty() y @ApiPropertyOptional() como vemos en el siguiente ejemplo:

// products-query.dto.ts
import { ApiPropertyOptional } from "@nestjs/swagger";

export class ProductsQueryDto {
  @ApiPropertyOptional({
    description: "Number of items to return",
    example: 10,
  })
  limit?: number;

  @ApiPropertyOptional({ description: "Filter cars by brand name" })
  brand?: string;
}

Luego al hacer uso del DTO que definimos para los query params, Swagger automáticamente documentará cada uno de ellos, nuestro controlador quedará más limpio como se ve en el siguiente fragmento:

@Get('/cars')
findAll(@Query() query: ProductsQueryDto) {
// Swagger automaticament documenta 'limit' y 'brand'
}

Documentar headers

En NestJS podemos documentar un header utilizando el decorador @ApiHeader() del paquete @nestjs/swagger.

El decorador @ApiHeader() recibe un objeto con los siguientes atributos:

Veamos un ejemplo:

@ApiHeader({
  name: 'x-correlation-id',
  description: 'Unique identifier used to trace the request',
  required: true,
  schema: {
    type: 'string',
    example: '558e0488-e29b-41d4-a716-446655440000'
  }
})

Nota: Si deseamos documentar varios headers en el mismo endpoint, solo tenemos que agregar múltiples decoradores @ApiHeader()

Por otra parte, si tenemos un header que se usa múltiples veces en diferentes lugares, podemos optar por generar una documentación reusable de headers creando nuestro propio decorador personalizado.

// api-correlation-id.decorator.ts

import { applyDecorators } from "@nestjs/common";
import { ApiHeader } from "@nestjs/swagger";

export function ApiCorrelationId() {
  return applyDecorators(
    ApiHeader({
      name: "x-correlation-id",
      description: "Unique identifier used to trace the request.",
      required: true,
      schema: {
        type: "string",
        example: "550e8400-e29b-41d4-a716-446655440000",
      },
    }),
  );
}

Luego en el controlador simplemente lo importamos y lo usamos de la siguiente manera:

import { ApiCorrelationId } from '@decorators/api-correlation-id.decorator';

@Get()
@ApiCorrelationId()
getUsers() {
  ...
}

En caso de que tengas múltiples implementaciones de decoradores personalizados, una estructura como la siguiente puede ser recomendable:

src/
├── common/
│   ├── decorators/
│   │  ├── swagger/
|   |  │   ├── api-correlation-id.decorator.ts
|   |  │   ├── api-client-version.decorator.ts
|   |  │   ├── api-device-id.decorator.ts
|   |  │   └── index.ts
|   |  ├── auth/
|   |  │   └── ...
|   |  └── index.ts
│   ├── guards/
│   ├── interceptors/
│   ├── filters/
│   └── pipes/
│
├── modules/
│   ├── users/
│   │   ├── users.controller.ts
│   │   ├── users.service.ts
│   │   └── ...
│   └── auth/
│       └── ...
│
├── app.module.ts
└── main.ts

Observa que dentro del directorio swagger tenemos un archivo index.ts que agrupa y exporta todos los decoradores, con una sintaxis como la siguiente:

// src/common/decorators/swagger/index.ts

export * from "./api-correlation-id.decorator";
export * from "./api-client-version.decorator";
export * from "./api-device-id.decorator";

Por lo que al momento de importarlos la sintaxis será la siguiente:

import {
  ApiCorrelationId,
  ApiClientVersion,
  ApiDeviceId,
} from "@/common/decorators/swagger";

Una nota final a considerar es que si el header solo se reutiliza unicamente dentro de los endpoints expuestos en un controller y no en los demás, deberíamos entonces ubicar los decoradores personalizados, dentro del módulo que hace uso de el, de este modo el directorio common se mantendrá más limpio y unicamente tendrá implementaciones realmente compartidas.

Documentar rutas protegidas con Bearer Token

La autorización es un header más. Sin embargo, no solemos documentar el header de autorización que usa un Bearer Token usando el decorador @ApiHeader(), sino uno de los decoradores dedicados de @nestjs/swagger. El decorador ApiBearerAuth().

import { ApiBearerAuth } from '@nestjs/swagger';

@Get()
@ApiBearerAuth()
getUsers() {
  ...
}

Para que la documentación describa correctamente el esquema de autorización con Bearer Token y agregue automáticamente el botón de Authorize. Debemos agregar el método .addBearerAuth() en la configuración de Swagger en el archivo main.ts.

const config = new DocumentBuilder()
  .setTitle("My API")
  .setDescription("API documentation")
  .setVersion("1.0")
  .addBearerAuth()
  .build();

Documentar el Body de la petición

Cuando realizamos la descripción de los atributos en un DTO estamos ya adelantando la documentación que describe el payload que llega a cada uno de nuestros endpoints, tenemos entonces dos formas de documentar el cuerpo de una petición, utilizando el decorador @ApiBody() o dejando que Swagger procese el parámetro body de la función handler. Veamos ambos casos.

Podemos definir un DTO y usarlo de forma explícita en el decorador @ApiBody() y pasando el DTO en el atributo type del objeto de configuración. Este es un caso típico, por ejemplo, en el endpoint /loging donde es PassportJS el que hace el procesamiento del cuerpo de la petición y nosotros no tenemos que usar el contenido de ese body.

@ApiBody({ type: LoginDTO })
@UseGuards(AuthGuard('local'))
@Post('login')
login(@Req() req: Request) {
    const user = req.user as User;
    ...
  };
}

Por otra parte, la documentación típica de un body es bastante automática al sar el DTO como prámetro en el handler del controlador, veamos un ejemplo a continuación:

@Post()
create(@Body() createPostDto: CreatePostDto) {
  ...
}

En este caso, Swagger extraerá toda la información desde el DTO createPostDto y la utilizará para nutrir la información del endpoint.

Descripción de las respuestas

Podemos describir posibles respuestas de nuestro endpoint mediante el decorador @ApiResponse() que puede recibir los siguiente atributos en su objeto de configuración:

Un ejemplo de la descripción de respuestas se muestra a continuación:

export class UsersController {
  @Get()
  @ApiResponse({
    status: 200,
    description: "The users have been successfully retrieved.",
    type: UserDto,
    isArray: true,
  })
  findAll() {
    return [];
  }
}

También es importante mencionar que NestJS nos ofrece una gran lista de API responses predefinidas que nos pueden ahorrar mucho tiempo, una parte de la lista se muestra a continuación:

Un ejemplo que muestra el uso de los decoradores predefinidos se muestra a continuación:

@Post()
@ApiCreatedResponse({ description: 'The record has been successfully created.'})
@ApiForbiddenResponse({ description: 'Forbidden.'})
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

Nota: Si queremos que nuestra documentación muestre ejemplos de respuesta de nuestro endpoint, debemos asegurarnos de pasar una clase en el atributo type del objeto de configuración que se pasa a @ApiResponse. Esta clase debe describir la forma de la respuesta, pensemos en un DTO de respuesta. Es una clase comentada con los decoradores @ApiProperty() y @ApiPropertyOptional().

Seguridad de la documentación

El mejor esquema para proteger la documentación de la aplicación es no activarla en el ambiente productivo, solamente en development y staging, y además, ponerla detrás de una VPN. Sin embargo, si nuestro proyecto no es aún tan grande como para levantar una estructura de ambientes, VPN y credenciales, sino que, estamos hablando de un proyecto piloto o una prueba de concepto, podemos valernos de un esquema básico de autenticación.

Para aplicaciones de NestJS que usan Express (lo que ocurre por defecto), de hecho, NestJS usa el paquete swagger-ui-express para la documentación de Swagger. Nosotros podemos agregar el paquete express-basic-auth que podemos instalar con la siguiente línea:

npm install express-basic-auth

o usando yarn:

yarn add express-basic-auth

Luego debemos ajustar la configuración en el archivo main.ts, allí vamos a agregar un middleware que se encargará de verificar si se está haciendo una request a las rutas /docs o /docs-json (que son nuestras rutas de documentación) y si es el caso, entonces solicitará un usuario y contraseña para poder liberar la documentación.

La configuración unicamente varia al agregar el middleware en la aplicación, veamos el ejemplo:

async function bootstrap() {
// Protect Swagger UI and Swagger JSON
  const swaggerUsername = configService.getOrThrow<string>('swagger.username');
  const swaggerPassword = configService.getOrThrow<string>('swagger.password');

  app.use(
    ['/docs', '/docs-json'],
    expressBasicAuth({
      challenge: true,
      users: {
        [swaggerUsername]: swaggerPassword
      }
    })
  );

  // Setups and build Swagger documentation
  const config = new DocumentBuilder()
    .setTitle('Blog API')
    .setDescription('The blog API description')
    .setVersion('1.0')
    .addTag('blog')
    .build();

  ...
}

Recuerda que como estamos usando el ConfigService debemos actualizar con las nuevas variables el archivo de ambiente .env, el schema validator y el loader, tal cómo vimos en el post sobre variables de entorno.

Ahora si levantas el servidor y vas a la ruta de documentación encontrarás que solicita las credenciales antes de poder visualizarlas. Recuerda elegir una contraseña con un buen nivel de complejidad. Observa que, aunque ahora solicitamos credenciales, no estamos protegidos ante ataques de fuerza bruta, es decir que un atacante podría enviar miles de peticiones tratando de adivinar nuestras credenciales. Este tópico no solo es importante sino que lo podemos mitigar protegiendo algunas rutas como el /loging usando el decorador @Throttle() del paquete @nestjs/throttle sino también configurando adecuadamente los rate limiters de Nginx. Todo esto lo veremos en futuros posts.

Eso fue todo por hoy, en esta entrada pudimos ver todos los mecanismos que podemos utilizar para generar una documentación de alta calidad para nuestra aplicación, vimos además como protegerla usando un esquema básico de usuario y contraseña para que la estructura de nuestro servicio no esté abierta públicamente en internet. Con esto finalizamos el apartado de documentación y nos vamos a mover para ver los pasos de configuración de nuestro proyecto de cara al despliegue en un ambiente de producción.


Autor: Julio César Echeverri Marulanda