Voltar para Artigos
Back-end★ Destaque11 min de leitura

Documentação Automatizada de APIs com TSOA e Swagger

TSOA gerando OpenAPI 3.0 a partir de decorators TypeScript: controllers tipados, validação de body com @Body, autenticação documentada, responses de erro e integração com Zod para validação em runtime.

12 de agosto de 2026

Documentação desatualizada é pior que documentação ausente: leva o desenvolvedor a confiar em informações erradas. O problema com swagger.json mantido manualmente é que a API muda, os tipos mudam, e o desenvolvedor esquece de atualizar. Em 6 meses, a documentação está descrevendo uma API que não existe mais.

O TSOA (TypeScript OpenAPI) inverte essa relação: a documentação é gerada a partir do código TypeScript. Decorators nos controllers, tipos nas interfaces, JSDoc nos métodos — tudo vira OpenAPI 3.0 automaticamente. Se o tipo mudar, a documentação muda junto na próxima geração.

Outra vantagem crítica é que o TSOA valida os dados de entrada em runtime usando os mesmos tipos TypeScript que você já declarou para documentação. Você escreve a interface CreateUserDTO uma vez, e o TSOA garante tanto que o Swagger mostre o schema correto quanto que requests com payload inválido retornem 400 automaticamente — sem um middleware de validação separado.

Instalação e Configuração

bash
npm install tsoa swagger-ui-express
npm install -D @types/swagger-ui-express
tsoa.json
{
  "entryFile": "src/server.ts",
  "noImplicitAdditionalProperties": "throw-on-extras",
  "controllerPathGlobs": ["src/modules/**/controllers/*.controller.ts"],
  "spec": {
    "outputDirectory": "src",
    "specVersion": 3,
    "name": "Minha API",
    "description": "Documentação da API REST",
    "version": "1.0.0",
    "contact": {
      "name": "Time de Engenharia",
      "email": "eng@empresa.com"
    },
    "securityDefinitions": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    }
  },
  "routes": {
    "routesDir": "src",
    "authenticationModule": "src/middlewares/tsoa-authentication.ts"
  }
}
package.json (scripts)
{
  "scripts": {
    "tsoa:spec": "tsoa spec",
    "tsoa:routes": "tsoa routes",
    "tsoa:gen": "tsoa spec && tsoa routes",
    "build": "npm run tsoa:gen && tsc",
    "dev": "npm run tsoa:gen && ts-node-dev src/server.ts"
  }
}

DTOs Tipados: A Base da Documentação

O TSOA extrai a documentação diretamente das interfaces TypeScript. Defina DTOs claros para cada operação. Os comentários JSDoc (/** ... */) são lidos pelo TSOA e viram descrições nos campos do Swagger. As anotações como @minLength e @pattern funcionam tanto como documentação quanto como regras de validação em runtime.

UserDTO.ts
/** Dados necessários para criar um novo usuário */
export interface CreateUserDTO {
  /** Nome completo do usuário */
  name: string;

  /** E-mail único. Será usado para login */
  email: string;

  /**
   * Senha do usuário.
   * Mínimo de 8 caracteres, deve conter letras e números.
   * @minLength 8
   * @pattern ^(?=.*[A-Za-z])(?=.*\d).+$
   */
  password: string;

  /** Função do usuário no sistema */
  role?: 'USER' | 'MANAGER' | 'ADMIN';
}

/** Representação pública do usuário (sem senha) */
export interface UserDTO {
  id: string;
  name: string;
  email: string;
  role: string;
  emailVerified: boolean;
  createdAt: string; // ISO 8601
}

/** Resposta paginada de usuários */
export interface PaginatedUsersDTO {
  data: Array<UserDTO>;
  total: number;
  page: number;
  pageSize: number;
  totalPages: number;
}

Controller TSOA Completo

O Controller do TSOA combina declaração de rotas, documentação OpenAPI e validação em uma única classe. Os decorators @Route, @Get, @Post definem os endpoints. @Security('bearerAuth') documenta que a rota requer autenticação E faz o TSOA chamar o módulo expressAuthentication automaticamente antes do controller. @Response<ErrorDTO>(404, '...') documenta as respostas de erro no Swagger sem código adicional.

users.controller.ts
import {
  Controller, Get, Post, Put, Delete,
  Route, Tags, Path, Body, Query,
  Response, SuccessResponse, Security, Request
} from 'tsoa';
import type { Request as ExpressRequest } from 'express';
import type { CreateUserDTO, UserDTO, PaginatedUsersDTO } from './dtos/UserDTO';
import type { ErrorDTO } from '../../dtos/ErrorDTO';

@Route('users')
@Tags('Users')
export class UsersController extends Controller {

  /**
   * Lista todos os usuários com paginação e filtros opcionais.
   * Requer autenticação e permissão 'users.read'.
   */
  @Get()
  @Security('bearerAuth') // Documenta que a rota requer Bearer Token
  @Response<ErrorDTO>(401, 'Não autenticado')
  @Response<ErrorDTO>(403, 'Sem permissão')
  public async list(
    @Query() page: number = 1,
    @Query() pageSize: number = 20,
    @Query() search?: string,
    @Query() role?: 'USER' | 'MANAGER' | 'ADMIN'
  ): Promise<PaginatedUsersDTO> {
    const useCase = makeListUsersUseCase();
    return useCase.execute({ page, pageSize, search, role });
  }

  /**
   * Busca um usuário específico pelo ID.
   * @param id UUID do usuário
   */
  @Get('{id}')
  @Security('bearerAuth')
  @Response<ErrorDTO>(404, 'Usuário não encontrado')
  public async show(@Path() id: string): Promise<UserDTO> {
    const useCase = makeFindUserByIdUseCase();
    return useCase.execute(id);
  }

  /**
   * Cria um novo usuário no sistema.
   * O e-mail deve ser único.
   */
  @Post()
  @SuccessResponse(201, 'Usuário criado com sucesso')
  @Response<ErrorDTO>(400, 'Dados inválidos')
  @Response<ErrorDTO>(409, 'E-mail já cadastrado')
  public async create(@Body() body: CreateUserDTO): Promise<UserDTO> {
    this.setStatus(201);
    const useCase = makeCreateUserUseCase();
    return useCase.execute(body);
  }

  /**
   * Deleta um usuário. Apenas ADMINs podem realizar esta operação.
   * @param id UUID do usuário a ser deletado
   */
  @Delete('{id}')
  @Security('bearerAuth')
  @SuccessResponse(204, 'Usuário deletado')
  @Response<ErrorDTO>(403, 'Apenas ADMINs podem deletar usuários')
  @Response<ErrorDTO>(404, 'Usuário não encontrado')
  public async delete(@Path() id: string, @Request() req: ExpressRequest): Promise<void> {
    this.setStatus(204);
    const useCase = makeDeleteUserUseCase();
    await useCase.execute({ targetId: id, requesterId: req.user.id });
  }
}

Módulo de Autenticação para o TSOA

tsoa-authentication.ts
import type { Request } from 'express';
import { verify } from 'jsonwebtoken';

// Este arquivo é referenciado em tsoa.json -> routes.authenticationModule
// O TSOA o chama automaticamente para rotas marcadas com @Security
export async function expressAuthentication(
  request: Request,
  securityName: string,
  _scopes?: Array<string>
): Promise<unknown> {
  if (securityName === 'bearerAuth') {
    const authHeader = request.headers.authorization;

    if (!authHeader?.startsWith('Bearer ')) {
      throw { status: 401, message: 'Token não fornecido.' };
    }

    const token = authHeader.slice(7);

    try {
      const payload = verify(token, process.env.JWT_SECRET as string);
      return payload; // O retorno é atribuído a request.user pelo TSOA
    } catch {
      throw { status: 401, message: 'Token inválido ou expirado.' };
    }
  }

  throw { status: 401, message: 'Método de autenticação não reconhecido.' };
}

Servindo o Swagger UI no Express

server.ts
import express from 'express';
import swaggerUi from 'swagger-ui-express';
// O TSOA gera este arquivo automaticamente via 'tsoa spec'
import swaggerDocument from './swagger.json';
// O TSOA gera este arquivo automaticamente via 'tsoa routes'
import { RegisterRoutes } from './routes'; // Generated

const app = express();
app.use(express.json());

// Swagger UI — apenas fora de produção (ou com autenticação)
if (process.env.NODE_ENV !== 'production') {
  app.use(
    '/api-docs',
    swaggerUi.serve,
    swaggerUi.setup(swaggerDocument, {
      swaggerOptions: {
        persistAuthorization: true, // Mantém o token após refresh da página
      },
    })
  );

  // Expõe o JSON do spec para consumo por ferramentas externas (Insomnia, Postman)
  app.get('/api-docs/swagger.json', (_req, res) => res.json(swaggerDocument));
}

// Registra todas as rotas geradas pelo TSOA
RegisterRoutes(app);

export { app };

A principal vantagem do TSOA sobre o swagger-jsdoc manual é que o TypeScript garante a consistência: se você mudar o tipo de UserDTO.email de string para string | null, o Swagger será atualizado automaticamente na próxima geração. Com swagger-jsdoc, você esqueceria de atualizar os comentários YAML.

Conclusão

O TSOA transforma TypeScript em documentação OpenAPI 3.0 sem duplicação de esforço. Os tipos já existem no código — o TSOA apenas os lê e os converte em um contrato que frontends, parceiros e equipes podem consumir. O comando tsoa spec rodando antes de cada build garante que a documentação publicada sempre reflete o código em produção.