NestJS e Swagger: Automatize a geração de documentação para sua API

Desenvolver uma API é apenas o primeiro passo. Para que essa API seja utilizada por outros desenvolvedores, é fundamental que ela seja bem documentada. É necessário especificar de alguma forma, como a API funciona, quais são seus recursos e como consumi-los. Nesse contexto, a especificação OpenAPI oferece um padrão para descrever APIs, facilitando seu entendimento e o consumo.

OpenAPI Specification (formerly Swagger Specification) is an API description format for REST APIs. An OpenAPI file allows you to describe your entire API, including:

Available endpoints (/users) and operations on each endpoint (GET /usersPOST /users)
Operation parameters Input and output for each operation
Authentication methods
Contact information, license, terms of use, and other information.

API specifications can be written in YAML or JSON. The format is easy to learn and readable to both humans and machines. The complete OpenAPI Specification can be found on GitHub: OpenAPI 3.0 Specification

https://swagger.io/docs/specification/v3_0/about/

Ao utilizar a especificação da OpenAPI, conhecida antigamente como especificação Swagger, é possível gerar uma documentação interativa que inclui informações detalhadas sobre os endpoints, os métodos HTTP suportados, os parâmetros de requisição e resposta, além de exemplos de uso. Essa documentação pode ser acessada através de uma interface visual, como o Swagger UI, que permite testar a API diretamente no navegador conforme a imagem abaixo ilustra. Também é possível gerar a documentação em um arquivo YAML ou JSON.

Exemplo do Swagger Editor: http://editor.swagger.io

Uma das formas de gerarmos essa documentação para uma API, é criando manualmente um arquivo JSON ou YAML. Contudo, os frameworks geralmente oferecem recursos que facilitam esse processo. No caso do NestJS, por exemplo, é possível instalar pacotes específicos e utilizar decorators para efetuar a documentação. Inclusive, este é o tema deste texto.

Criando uma API com NestJS

Para iniciar, o pequeno script abaixo gera uma aplicação NestJS:

# Versões:
node -v   
# v22.11.0
nest -v
# 10.4.7

# Criando o projeto
nest new \
  --skip-git \
  -p npm \
  -l typescript \
  --strict \
  exemplo-swagger

# Entrando no diretório do novo projeto
cd exemplo-swagger/

# Executando o projeto
npm run start:dev

# Testando o projeto com cURL
curl -i localhost:3000
#
# Resultado:
#
# HTTP/1.1 200 OK
# X-Powered-By: Express
# Content-Type: text/html; charset=utf-8
# Content-Length: 12
# 
# Hello World!

Configurando o Swagger

Após criar a aplicação, é necessário instalar alguns pacotes conforme o seguinte script:

npm install --save @nestjs/swagger

# Também instalei os pacotes para
# ajudar na validação de dados. Se
# quiser saber mais sobre esse assunto:
# https://consolelog.com.br/validando-dados-api-class-validator-class-transformer-nestjs
npm i --save class-validator class-transformer

Uma vez instalado os pacotes acima, é necessário configurar o Swagger no módulo principal da aplicação, importando o SwaggerModule e DocumentBuilder conforme a seguir:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

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

  // ##################################################
  const config = new DocumentBuilder()
    .setTitle('API de Estudo')
    .setDescription('Documentação - consolelog.com.br')
    .setVersion('1.0')
    .build();

  const documentFactory = () => SwaggerModule
    .createDocument(app, config);

  SwaggerModule.setup('api', app, documentFactory);
  // ##################################################
  
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

main.ts

Com apenas essa pequena configuração, você já pode visualizar a documentação Swagger gerada. Para isso, execute a aplicação com o comando npm run start:dev e acesse o endereço http://localhost:3000/api no navegador.

Exemplo da interface interativa do Swagger

Como a aplicação contém um único endpoint (GET /), somente ele foi mapeado na documentação, conforme a imagem acima ilustra. Continuando os estudos, vamos aprender a detalhar melhor este único endpoint.

Como documentar um endpoint com os decorators do swagger

Através de uma série de decorators, é possível por exemplo, descrever um método, relatar quais são os possíveis retornos, quais são as query strings presentes, entre outros.

No código abaixo foi utilizado o ApiOperation e ApiResponse para descrever o endpoint GET / e documentar os possíveis retornos.

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';

@ApiTags('AppController')
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  @ApiOperation({
    summary: 'Endpoint criado pelo CLI do NestJS',
    description: 'Retorna uma mensagem fixa para testes'
  })
  @ApiResponse({
    status: 200,
    description: 'Retorna uma mensagem fixa para testes',
    type: String
  })
  @ApiResponse({
    status: 500,
    description: 'Erro interno'
  })
  getHello(): string {
    return this.appService.getHello();
  }
}

app.controller.ts

Após aplicar os decorators, os detalhes são refletidos na documentação:

Exemplo de como a documentação feita através dos decorators é exibida no Swagger

A seguir, será abordado cenários mais próximos ao cotidiano, como a documentação de rotas que envolvem, query string, parâmetros de URL, corpo (body) e cabeçalhos de requisição e resposta.

Como documentar query string, corpo de resposta e um cabeçalho de requisição e resposta no swagger

No exemplo a seguir, é apresentado o endpoint GET /usuarios. Para fins de teste, esta rota exige o cabeçalho x-request-teste. Além disso, inclui a query string opcional nome.

Como esta rota retorna uma lista de usuários, foi necessário criar a classe UsuarioModel e documentar os campos conforme abaixo.

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

export class UsuarioModel {
  @ApiProperty({
    description: 'Identificador único do usuário'
  })
  id: number;

  @ApiProperty({
    description: 'Nome do usuário'
  })
  nome: string;

  @ApiProperty({
    description: 'Email do usuário',
  })
  email: string;
}

usuario.model.ts

No método find, dentro de UsuariosController, foram adicionados alguns decorators para documentar o endpoint:

@Get()
@ApiOperation({
  summary: 'Lista de usuários',
  description: 'Permite pesquisar usuários cadastrados',
})
@ApiQuery({
  name: 'nome',
  description: 'Filtrar por nome',
  required: false,
  type: String,
})
@ApiHeader({
  name: 'x-request-teste',
  description: 'Exemplo cabeçalho de requisição',
  required: true,
})
@ApiResponse({
  status: 200,
  description: 'Lista de usuários',
  isArray: true,
  type: UsuarioModel,
  headers: {
    'x-teste': {
      description: 'Exemplo do cabeçalho de resposta',
      example: 'teste',
    },
  },
})
find(
  @Query('nome') nome: string,
  @Headers('x-request-teste') xRequestTeste: string,
  @Res() res: Response,
) {
  res.setHeader('x-teste', xRequestTeste);
  res.json(this.usuarioService.find(nome));
}

usuarios.controller.ts (trecho)

Observe no resultado que cada decorator representa um trecho na documentação. Por exemplo, o ApiHeader descreve um cabeçalho que é esperado na requisição, já o ApiResponse detalha como será a resposta do endpoint para um determinado http status code:

Exemplo de como as query strings, headers e body de resposta são exibidos.

Como incluir parâmetros de URL no Swagger

Outro cenário comum, é o uso de parâmetros na URL. Por exemplo, GET /usuarios/:id. Neste caso podemos utilizar o decorator ApiParam para descrever os parâmetros:

@Get(':id')
@ApiOperation({
  summary: 'Obtém um usuário específico',
  description: 'Retorna os dados de um usuário',
})
@ApiParam({
  name: 'id',
  description: 'Id do usuário',
  required: true,
  type: Number,
})
@ApiResponse({
  status: 200,
  description: 'Dados do usuário',
  type: UsuarioModel,
})
@ApiResponse({
  status: 404,
  description: 'Usuário não encontrado',
  type: UsuarioModel,
})
findOne(@Param('id', ParseIntPipe) id: number): UsuarioModel {
  const usuario = this.usuarioService.findOne(id);
  if (usuario === undefined) {
    throw new NotFoundException();
  }

  return usuario;
}

usuarios.controller.ts (trecho)

Exemplo de como o parâmetro de URL, id, é apresentado na documentação

Como documentar o corpo de requisição (request body)

Normalmente utilizando os método POST, PUT ou PATCH, é comum ter um corpo de requisição, normalmente um JSON. Para documentar esse request body, podemos criar uma classe e aplicar tanto os decorators de validação, que já foram abordados por aqui, quanto os do Swagger. Observe no código a seguir:

import { ApiProperty } from "@nestjs/swagger";
import {
  IsEmail,
  IsNotEmpty,
  IsString
} from "class-validator";

export class CreateUserDto {
  @ApiProperty()
  @IsString()
  @IsNotEmpty()
  nome: string;

  @ApiProperty()
  @IsEmail()
  email: string;
}

create-user.dto.ts

Já no método dentro do controller, a documentação é muito semelhante aos outros endpoints apresentados anteriormente. Basta especificar o tipo da classe recebida como parâmetro do método e incluir o decorator@Body do NestJS:

@Post()
@ApiOperation({
  summary: 'Cria um usuário',
  description: 'Faz o registro de um novo usuário',
})
@ApiResponse({
  status: 201,
  description: 'Dados do usuário recém-criado',
  type: UsuarioModel,
})
@ApiResponse({
  status: 400,
  description: 'Dados inválidos',
})
@UsePipes(new ValidationPipe({ whitelist: true }))
create(@Body() body: CreateUserDto) {
  return this.usuarioService.create(body);
}

usuarios.controller.ts (trecho)

Exemplo de como o corpo (body) de requisição é documentado

Como documentar o token de acesso

É extremamente comum que haja um processo de autenticação e autorização em APIs. A mecânica básica é através da utilização de tokens de acesso. Após o usuário se autenticar (por exemplo, fornecendo um nome de usuário e senha), a API gera um token, que é enviado ao usuário.

Em todas as requisições subsequentes, o usuário inclui o token de acesso no cabeçalho da requisição. A API verifica a validade do token e, se estiver válido, autoriza o usuário a realizar a ação solicitada.

Para simular esse cenário no projeto de estudo deste texto, foi criado uma classe chamada AuthGuard, que é responsável por validar um valor fixo no cabeçalho de requisição authentication.

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const req = context.switchToHttp().getRequest();
    const authHeader = req.headers.authorization;

    if (!authHeader) {
      throw new UnauthorizedException();
    }

    if (authHeader !== 'Bearer teste') {
      throw new UnauthorizedException();
    }

    return true;
  }
}

auth.guard.ts

Essa classe foi registrada no provide APP_GUARD. Isso significa que antes de executar o método de cada rota, a classe AuthGuard é executada para validar se o token de acesso está presente e é válido.

// importsn...

@Module({
  controllers: [AppController, UsuariosController],
  providers: [
    AppService,
    UsuarioService,
    // ###################
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
    // ###################
  ],
})
export class AppModule {}

app.module.ts

Agora que todos os endpoints estão protegidos, para documentar isto no swagger é necessário chamar o método .addBearerAuth() durante a configuração do DocumentBuilder do Swagger:

// imports...

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

  const config = new DocumentBuilder()
    .setTitle('API de Estudo')
    .setDescription('Documentação - consolelog.com.br')
    .setVersion('1.0')
    .addBearerAuth()
    .build();

  const documentFactory = () => SwaggerModule
    .createDocument(app, config);

  SwaggerModule.setup('api', app, documentFactory);
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

main.ts

Para indicar na documentação do Swagger quais endpoints estão protegidos, basta adicionar o decorator @ApiBearerAuth() ao método ou ao controller. Quando aplicado no controller, todos os endpoints contidos nele serão documentados com o filtro de autenticação.

// imports...

@ApiTags('Usuarios')
@ApiBearerAuth()
@Controller('/usuarios')
export class UsuariosController {
  // ...código ocultado
}

usuarios.controller.ts (trecho)

Após essa configuração, ao executar o projeto e acessar a documentação no endereço http://localhost:3000/api, será exibido o botão "Authorize" no canto superior direito, além de um ícone de cadeado ao lado de cada endpoint protegido. Ao clicar em "Authorize", você poderá inserir o token de acesso, permitindo consumir os endpoints diretamente pela interface visual no navegador.

Exemplo de como a documentação funciona em uma API protegida por um token de acesso

Organizando os decorators do swagger

O uso de decorators para documentação, como os do Swagger, em alguns casos pode tornar o código menos legível devido à alta densidade. Uma possível solução é agrupar esses decorators em um arquivo separado, criando um único decorator consolidado. Assim, basta importar esse agrupador no código. Confira o exemplo abaixo:

//
// Os decorators do endpoint POST /usuarios
// foram extraídos para este arquivo arquivo
// usuarios.swagger.ts
//
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
import { UsuarioModel } from './usuario.model';

export function ApiUsuariosPost(): MethodDecorator {
  return function (
    target: any,
    propertyKey: any,
    descriptor: any,
  ): void {
    ApiOperation({
      summary: 'Cria um usuário',
      description: 'Faz o registro de um novo usuário',
    })(target, propertyKey, descriptor);

    ApiResponse({
      status: 201,
      description: 'Dados do usuário recém-criado',
      type: UsuarioModel,
    })(target, propertyKey, descriptor);

    ApiResponse({
      status: 400,
      description: 'Dados inválidos',
    })(target, propertyKey, descriptor);
  };
}

usuarios.swagger.ts

Após agrupar os decorators no decorator ApiUsuariosPost, conforme o código acima, basta importar o @ApiUsuariosPost() e adicioná-lo no método desejado:

// ...outros imports...
import { ApiUsuariosPost } from './usuarios.swagger';

@ApiTags('Usuarios')
@Controller('/usuarios')
export class UsuariosController {
  constructor(private usuarioService: UsuarioService) {}

  // ...código ocultado...

  @Post()
  @ApiUsuariosPost()
  @UsePipes(new ValidationPipe({ whitelist: true }))
  create(@Body() body: CreateUserDto) {
    return this.usuarioService.create(body);
  }
}

usuarios.controller.ts (trecho)

Gerando o JSON ou YAML do Swagger

Para gerar o Swagger no formato JSON ou YAML, basta incluir o seguinte trecho no arquivo main.ts:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import {
  DocumentBuilder,
  SwaggerModule,
} from '@nestjs/swagger';

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

  const config = new DocumentBuilder()
    .setTitle('API de Estudo')
    .setDescription('Documentação - consolelog.com.br')
    .setVersion('1.0')
    .addBearerAuth()
    .build();

  const documentFactory = () =>
    SwaggerModule.createDocument(app, config);

  SwaggerModule.setup('api', app, documentFactory);
  // ###################################################
  SwaggerModule.setup('swagger', app, documentFactory, {
    jsonDocumentUrl: 'swagger/json',
    yamlDocumentUrl: 'swagger/yaml',
  });
  // ###################################################

  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

Feito isto, após executar o projeto é possível acessar as rotas /swagger/json ou /swagger/yaml para obter a documentação no formato desejado.

Considerações

O uso do Swagger na documentação de APIs proporciona detalhes de forma clara, interativa e facilmente acessível para os desenvolvedores. Ele simplifica o entendimento e a comunicação sobre os endpoints disponíveis, seus parâmetros, retornos e requisitos de autenticação. Além disso, a integração do NestJS com o Swagger, por meio de decorators, torna o processo de documentação bem simples.

Abaixo deixo alguns links interessantes sobre o assunto: