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.
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
npm install tsoa swagger-ui-express
npm install -D @types/swagger-ui-express{
"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"
}
}{
"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.
/** 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.
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
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
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.