Aula 14 — Documentação com Swagger
🎯 Objetivos de aprendizagem¶
Ao final desta aula você será capaz de:
- Explicar por que documentar uma API é parte do trabalho de engenharia, não um extra opcional.
- Diferenciar OpenAPI (a especificação) de Swagger (o conjunto de ferramentas que a implementa).
- Ler e escrever a anatomia de um documento OpenAPI 3.0:
info,servers,paths,components.schemas,components.securitySchemes. - Gerar a especificação a partir de anotações
@openapicomswagger-jsdoc, usando corretamente a chavedefinition. - Servir e customizar o Swagger UI com
swagger-ui-express, incluindo o endpoint com o JSON cru. - Documentar todos os endpoints do UniEventos com schemas reutilizáveis e segurança via
bearerAuth. - Testar endpoints protegidos direto pelo Swagger UI usando o botão "Authorize".
- Produzir um README de qualidade, um
CONTRIBUTING.mdmínimo e registrar decisões de arquitetura em formato ADR.
📋 Pré-requisitos desta aula¶
unieventos-api(ou projeto autoral) já refatorado para arquitetura em camadas (Aula 13), com rotas de eventos, inscrições e autenticação funcionando.- Node.js 22 LTS e a API rodando localmente com
npm run dev.
Checklist antes de começar:
- [ ]
GET /healthresponde200na sua API. - [ ] Você consegue autenticar via Firebase e obter um token de ID (Aula 10) para testar rotas protegidas.
- [ ] Ferramenta para chamadas HTTP manuais disponível (Insomnia, Postman ou
curl).
🗺️ Roteiro¶
| Bloco | Tempo | Atividade |
|---|---|---|
| 1 | 50 min | Por que documentar, OpenAPI vs. Swagger, anatomia do documento |
| 2 | 50 min | swagger-jsdoc + swagger-ui-express: configuração e primeiros endpoints anotados |
| 3 | 50 min | Documentando toda a API do UniEventos, segurança com bearerAuth, README/ADR |
Retomando a Aula 13¶
Na Aula 13 transformamos o unieventos-api em uma arquitetura em camadas testável e segura. O código ficou sólido por dentro — mas de fora, para quem nunca viu o projeto (um colega de equipe, um avaliador, você mesmo em três meses), ele ainda é uma caixa-preta: só descobre o que a API faz lendo o código-fonte inteiro. Hoje resolvemos isso com um contrato formal e navegável: OpenAPI + Swagger UI.
1. Por que documentar uma API¶
Um endpoint sem documentação obriga quem for consumi-lo a ler o código-fonte do back-end inteiro — ou pior, a adivinhar por tentativa e erro. Em qualquer cenário além do "eu programando sozinho e lembrando de tudo", isso custa tempo real:
- Contrato entre times. O time de front-end pode começar a construir a tela de "Minhas inscrições" antes do endpoint estar pronto, desde que o contrato (formato de entrada/saída) esteja documentado e estável. Documentação é o que permite front e back trabalharem em paralelo.
- Onboarding. Um novo integrante do time entende a API lendo uma página, não vasculhando 40 arquivos de rota.
- Geração de clientes. A partir de um documento OpenAPI, ferramentas geram automaticamente SDKs tipados em várias linguagens — você escreve o contrato uma vez, o cliente sai de graça.
- Contrato como teste. Ferramentas de teste de contrato conferem se a resposta real da API bate com o que foi documentado — a documentação vira uma fonte de verdade verificável, não um texto que fica defasado.
⚠️ Atenção Documentação que não é gerada a partir do código (ou vinculada a ele por anotação) apodrece rápido: alguém muda um campo na rota e esquece de atualizar o Word/Notion separado. É exatamente esse problema que o
swagger-jsdocresolve — a documentação vive ao lado do código, no mesmo arquivo, na mesma revisão de código.
1.1 OpenAPI vs. Swagger — não são sinônimos¶
- OpenAPI é a especificação: um formato (YAML ou JSON) que descreve endpoints, parâmetros, corpos de requisição, respostas e esquemas de segurança de uma API REST, de forma independente de linguagem. A versão usada nesta disciplina é a OpenAPI 3.0.
- Swagger é o conjunto de ferramentas (hoje mantido pela SmartBear) construído em torno da especificação OpenAPI — o nome "Swagger" é anterior ao nome "OpenAPI" (a especificação se chamava Swagger Specification até a versão 2.0; a partir da 3.0 passou a se chamar OpenAPI, mas o ecossistema de ferramentas manteve o nome Swagger).
Duas ferramentas do ecossistema Swagger que usaremos hoje:
| Ferramenta | Papel |
|---|---|
swagger-jsdoc |
Lê anotações @openapi em comentários JSDoc no seu código e gera o documento OpenAPI (JSON) |
swagger-ui-express |
Recebe esse documento OpenAPI e renderiza uma interface HTML interativa (o "Swagger UI") |
O swagger-ui é a interface visual que você provavelmente já viu em várias APIs públicas — aquela página com os endpoints agrupados por tag, cada um expansível, com botão "Try it out" para testar direto do navegador.
2. Anatomia de um documento OpenAPI 3.0¶
Um documento OpenAPI é um único objeto JSON (ou YAML) com estas chaves de topo:
openapi: 3.0.0 # versão da especificação usada
info: # metadados da API
title: UniEventos API
version: 1.0.0
description: API de eventos acadêmicos do UniEventos
servers: # onde a API está hospedada (pode ter vários)
- url: http://localhost:3000
description: Ambiente local
tags: # agrupamento visual dos endpoints no Swagger UI
- name: Eventos
- name: Inscrições
- name: Autenticação
paths: # cada endpoint documentado
/api/eventos:
get: { ... }
post: { ... }
components: # peças reutilizáveis entre paths
schemas: { ... } # formatos de objeto (Evento, EventoInput, Erro...)
securitySchemes: { ... } # como a API autentica (ex.: bearerAuth)
Explicando cada bloco com o UniEventos:
openapi— string fixa3.0.0, indica a versão da especificação. Não confunda com a versão da sua API (isso éinfo.version).info— título, versão e descrição da API. É o que aparece no topo do Swagger UI.servers— lista de URLs onde a API responde de verdade. Em desenvolvimento,http://localhost:3000; em produção, a URL pública (Aula 15). O Swagger UI usa isso para montar a URL completa quando você clica em "Try it out".tags— só organiza visualmente os endpoints em grupos colapsáveis (Eventos, Inscrições, Autenticação).paths— o coração do documento: cada rota (/api/eventos,/api/eventos/{id}...) e, dentro dela, cada método HTTP (get,post,put,delete), comparameters,requestBodyeresponses.components.schemas— formatos de objeto reutilizáveis (o formato de umEvento, de umEventoInput, de umErropadrão), referenciados de dentro depathscom$refem vez de repetidos em cada endpoint.components.securitySchemes— descreve como a API autentica (aqui, Bearer Token JWT do Firebase), sem misturar isso com a lógica de cada endpoint individual.parameters— parâmetros de path ({id}), query (?categoria=palestra) ou header, com tipo e descrição.requestBody— o formato esperado do corpo da requisição (POST/PUT), normalmente referenciando um schema via$ref.responses— para cada status HTTP possível (200,400,404...), o formato do corpo de resposta.$ref— mecanismo de referência: em vez de repetir a definição deEventoem 5 endpoints diferentes, cada um aponta para#/components/schemas/Evento. Mude uma vez, atualiza em todo lugar.
3. Duas abordagens para gerar o documento¶
3.1 Abordagem (a): anotações @openapi com swagger-jsdoc — a que vamos implementar¶
A ideia: você escreve um comentário JSDoc especial, com bloco YAML dentro, logo acima da definição da rota no próprio arquivo de rotas. O swagger-jsdoc varre os arquivos configurados, extrai esses comentários e monta o documento OpenAPI completo em tempo de execução.
npm install swagger-jsdoc swagger-ui-express
// src/docs/swaggerSpec.js
import swaggerJsdoc from 'swagger-jsdoc'
// ATENÇÃO: a chave é "definition", NÃO "swaggerDefinition" — swagger-jsdoc 6.x
// renomeou essa chave em relação a versões anteriores. Usar o nome errado faz
// a spec sair vazia, sem erro nenhum no console.
const opcoes = {
definition: {
openapi: '3.0.0',
info: {
title: 'UniEventos API',
version: '1.0.0',
description:
'API REST da plataforma UniEventos — divulgação e inscrição em eventos acadêmicos. ' +
'Desenvolvida na disciplina FACET-SNP-310 (UNEMAT/Sinop).',
contact: {
name: 'Prof. Ivan Luiz Pedroso Pires',
email: 'ivanpires@gmail.com',
},
license: {
name: 'MIT',
},
},
servers: [
{ url: 'http://localhost:3000', description: 'Ambiente local' },
{ url: 'https://unieventos-api.onrender.com', description: 'Produção' },
],
tags: [
{ name: 'Eventos', description: 'Cadastro e consulta de eventos acadêmicos' },
{ name: 'Inscrições', description: 'Inscrição de usuários autenticados em eventos' },
{ name: 'Autenticação', description: 'Fluxo de login com Firebase Auth' },
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'Token de ID do Firebase Auth, obtido após o login no front-end.',
},
},
},
},
// Arquivos onde o swagger-jsdoc procura comentários @openapi.
apis: ['./src/routes/*.js', './src/docs/schemas/*.js'],
}
export const swaggerSpec = swaggerJsdoc(opcoes)
⚠️ Atenção Repare na chave
definitiondentro deopcoes. Em versões antigas doswagger-jsdoc(2.x/3.x) essa chave se chamavaswaggerDefinition. Nesta disciplina usamosswagger-jsdoc@6.3.0, que exigedefinition. Se você copiar um tutorial antigo da internet comswaggerDefinition, a spec gerada fica compaths: {}vazio e nenhum erro é lançado — o bug é silencioso.
3.2 Abordagem (b): openapi.yaml escrito à mão¶
A alternativa é escrever o documento OpenAPI inteiro em um arquivo .yaml, sem anotação nenhuma no código, e servir esse arquivo estático:
# openapi.yaml (resumo — não é o que vamos usar hoje, é só para você conhecer a alternativa)
openapi: 3.0.0
info:
title: UniEventos API
version: 1.0.0
paths:
/api/eventos:
get:
tags: [Eventos]
summary: Lista eventos
responses:
'200':
description: Lista de eventos
// server.js — servindo o YAML escrito à mão, em vez de gerado por anotação
import { readFileSync } from 'node:fs'
import yaml from 'yaml' // npm install yaml
import swaggerUi from 'swagger-ui-express'
const documentoOpenApi = yaml.parse(readFileSync('./openapi.yaml', 'utf-8'))
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(documentoOpenApi))
Vantagem: controle total do texto, sem depender de comentário no meio do código. Desvantagem: fica fácil o YAML "descolar" do código real, porque nada obriga a atualizá-lo junto com a rota. Por isso, nesta disciplina, a abordagem oficial é a (a) — anotações junto ao código, sempre atualizadas na mesma revisão.
4. Servindo com swagger-ui-express¶
// src/app.js — trecho adicionado à montagem da aplicação (depois das rotas de negócio)
import swaggerUi from 'swagger-ui-express'
import { swaggerSpec } from './docs/swaggerSpec.js'
// Opções de customização visual do Swagger UI.
const opcoesDoSwaggerUi = {
customSiteTitle: 'UniEventos API — Documentação',
customCss: '.swagger-ui .topbar { display: none }', // esconde a barra verde padrão
}
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, opcoesDoSwaggerUi))
// Expõe o JSON cru da spec — útil para importar em Insomnia/Postman
// ou para ferramentas de geração de cliente consumirem diretamente.
app.get('/api-docs.json', (req, res) => {
res.status(200).json(swaggerSpec)
})
npm run dev
# abra no navegador:
# http://localhost:3000/api-docs → interface interativa
# http://localhost:3000/api-docs.json → JSON cru da especificação
💡 Dica
swaggerUi.serveé um array de middlewares (serve os arquivos estáticos da interface: CSS, JS, HTML);swaggerUi.setup(spec, opcoes)é o middleware que injeta sua spec nessa interface. Os dois sempre andam juntos, nessa ordem, no mesmoapp.use.
🧩 Padrão de projeto em uso¶
🧩 Padrão de projeto em uso — Decorator (documentação como anotação)¶
O padrão Decorator adiciona comportamento ou informação a um objeto sem alterar sua estrutura original. As anotações
@openapifazem exatamente isso, só que no nível de documentação de código-fonte em vez de tempo de execução: o comentário JSDoc "decora" a rota com metadados (parâmetros, respostas, segurança) sem alterar uma linha da lógica real dorouter.get(...). Remova o comentário e a rota continua funcionando idêntica — a documentação é uma camada adicionada por cima, não uma dependência funcional.
js /** * @openapi * /api/eventos: * get: * summary: Lista eventos ← "decoração": metadado * tags: [Eventos] ← "decoração": metadado */ router.get('/', eventosController.listar) // ← comportamento real, intocadoÉ a mesma lógica dos decorators de linguagens como TypeScript/Java (
@Component,@Test) — mas aqui implementada via convenção de comentário, lida por uma ferramenta externa (swagger-jsdoc), porque JavaScript puro (sem TypeScript) não tem decorators nativos estáveis no runtime do Node.
5. Documentando os schemas reutilizáveis¶
Antes de anotar cada rota, definimos os formatos de objeto que se repetem — assim cada endpoint só referencia ($ref) em vez de redigitar os mesmos campos.
// src/docs/schemas/evento.schema.js
/**
* @openapi
* components:
* schemas:
* Evento:
* type: object
* properties:
* id:
* type: integer
* example: 3
* titulo:
* type: string
* example: Hackathon FACET
* descricao:
* type: string
* example: Maratona de programação de 24 horas aberta a todos os cursos.
* categoria:
* type: string
* enum: [palestra, minicurso, workshop]
* example: workshop
* dataHora:
* type: string
* format: date-time
* example: 2026-10-05T08:00:00
* local:
* type: string
* example: Bloco A, Auditório
* vagas:
* type: integer
* example: 60
* imagemUrl:
* type: string
* format: uri
* example: https://storage.unieventos.dev/eventos/hackathon.jpg
*
* EventoInput:
* type: object
* required: [titulo, categoria, dataHora, local, vagas]
* properties:
* titulo:
* type: string
* minLength: 3
* maxLength: 150
* descricao:
* type: string
* categoria:
* type: string
* enum: [palestra, minicurso, workshop]
* dataHora:
* type: string
* format: date-time
* local:
* type: string
* vagas:
* type: integer
* minimum: 0
* imagemUrl:
* type: string
* format: uri
*
* Erro:
* type: object
* properties:
* mensagem:
* type: string
* example: Evento não encontrado
* detalhes:
* type: array
* items:
* type: object
* properties:
* campo:
* type: string
* mensagem:
* type: string
*
* Paginacao:
* type: object
* properties:
* pagina:
* type: integer
* example: 1
* porPagina:
* type: integer
* example: 20
* total:
* type: integer
* example: 47
*/
export {} // arquivo só existe para hospedar o comentário — sem código de fato
// src/docs/schemas/inscricao.schema.js
/**
* @openapi
* components:
* schemas:
* Inscricao:
* type: object
* properties:
* id:
* type: integer
* example: 12
* eventoId:
* type: integer
* example: 3
* usuarioUid:
* type: string
* example: fY3k9sLp2QaB1cD4eF5gH6iJ7kL8
* criadoEm:
* type: string
* format: date-time
*
* InscricaoInput:
* type: object
* required: [eventoId]
* properties:
* eventoId:
* type: integer
* example: 3
*/
export {}
🔎 Por baixo do capô Esses arquivos
*.schema.jsnão exportam nada útil em termos de código JavaScript — servem só para oswagger-jsdocencontrar o comentário (por isso estão incluídos emapis: [...]na configuração da Seção 3.1). É uma convenção comum para não poluir arquivos de rota reais com blocos de schema grandes.
6. Documentando todos os endpoints do UniEventos¶
6.1 Eventos — as 5 operações (CRUD completo)¶
// src/routes/eventos.routes.js — versão anotada
import { Router } from 'express'
import { validar } from '../middlewares/validar.js'
import { eventoSchema, eventoAtualizacaoSchema } from '../validators/eventoSchema.js'
import { verificarToken } from '../middlewares/autenticacao.js'
export function criarRotasDeEventos({ eventosController }) {
const router = Router()
/**
* @openapi
* /api/eventos:
* get:
* summary: Lista eventos, com filtros opcionais
* tags: [Eventos]
* parameters:
* - in: query
* name: categoria
* schema:
* type: string
* enum: [palestra, minicurso, workshop]
* description: Filtra por categoria do evento
* - in: query
* name: busca
* schema:
* type: string
* description: Filtra por trecho do título
* - in: query
* name: pagina
* schema:
* type: integer
* default: 1
* - in: query
* name: porPagina
* schema:
* type: integer
* default: 20
* responses:
* 200:
* description: Lista de eventos encontrados
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Evento'
*/
router.get('/', eventosController.listar)
/**
* @openapi
* /api/eventos/{id}:
* get:
* summary: Busca um evento pelo id
* tags: [Eventos]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Id numérico do evento
* responses:
* 200:
* description: Evento encontrado
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Evento'
* 404:
* description: Evento não encontrado
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Erro'
*/
router.get('/:id', eventosController.buscarPorId)
/**
* @openapi
* /api/eventos:
* post:
* summary: Cria um novo evento
* tags: [Eventos]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/EventoInput'
* responses:
* 201:
* description: Evento criado
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Evento'
* 400:
* description: Dados inválidos
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Erro'
* 401:
* description: Token ausente ou inválido
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Erro'
*/
router.post('/', verificarToken, validar(eventoSchema), eventosController.criar)
/**
* @openapi
* /api/eventos/{id}:
* put:
* summary: Atualiza um evento existente
* tags: [Eventos]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/EventoInput'
* responses:
* 200:
* description: Evento atualizado
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Evento'
* 404:
* description: Evento não encontrado
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Erro'
*/
router.put('/:id', verificarToken, validar(eventoAtualizacaoSchema), eventosController.atualizar)
/**
* @openapi
* /api/eventos/{id}:
* delete:
* summary: Remove um evento
* tags: [Eventos]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 204:
* description: Evento removido com sucesso, sem corpo de resposta
* 404:
* description: Evento não encontrado
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Erro'
*/
router.delete('/:id', verificarToken, eventosController.remover)
return router
}
6.2 Inscrições¶
// src/routes/inscricoes.routes.js — versão anotada
import { Router } from 'express'
import { validar } from '../middlewares/validar.js'
import { inscricaoSchema } from '../validators/inscricaoSchema.js'
import { verificarToken } from '../middlewares/autenticacao.js'
export function criarRotasDeInscricoes({ inscricoesController }) {
const router = Router()
/**
* @openapi
* /api/inscricoes:
* get:
* summary: Lista as inscrições do usuário autenticado
* tags: [Inscrições]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Lista de inscrições do usuário logado
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Inscricao'
* 401:
* description: Token ausente ou inválido
*/
router.get('/', verificarToken, inscricoesController.listarMinhas)
/**
* @openapi
* /api/inscricoes:
* post:
* summary: Inscreve o usuário autenticado em um evento
* tags: [Inscrições]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/InscricaoInput'
* responses:
* 201:
* description: Inscrição criada
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Inscricao'
* 409:
* description: Usuário já está inscrito neste evento
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Erro'
*/
router.post('/', verificarToken, validar(inscricaoSchema), inscricoesController.criar)
/**
* @openapi
* /api/inscricoes/{id}:
* delete:
* summary: Cancela uma inscrição do próprio usuário
* tags: [Inscrições]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 204:
* description: Inscrição cancelada
* 403:
* description: A inscrição pertence a outro usuário
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Erro'
*/
router.delete('/:id', verificarToken, inscricoesController.cancelar)
return router
}
6.3 Autenticação¶
O UniEventos não implementa login no back-end — o login acontece no front, direto contra o Firebase Auth (Aula 10). O back-end só verifica o token recebido. Ainda assim, documentamos esse fluxo, porque quem consumir a API precisa saber como obter o token:
// src/routes/autenticacao.routes.js
import { Router } from 'express'
export function criarRotasDeAutenticacao() {
const router = Router()
/**
* @openapi
* /api/auth/verificar:
* get:
* summary: Confirma se o token enviado é válido e devolve os dados do usuário
* description: >
* Não existe endpoint de login nesta API — o login acontece no front-end,
* diretamente contra o Firebase Auth (signInWithEmailAndPassword). Este
* endpoint serve apenas para confirmar que um token de ID do Firebase é válido.
* tags: [Autenticação]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Token válido
* content:
* application/json:
* schema:
* type: object
* properties:
* uid:
* type: string
* email:
* type: string
* 401:
* description: Token ausente, expirado ou inválido
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Erro'
*/
router.get('/verificar', (req, res) => {
res.status(200).json({ uid: req.usuario.uid, email: req.usuario.email })
})
return router
}
7. Segurança com bearerAuth e o botão "Authorize"¶
O esquema bearerAuth já foi declarado em components.securitySchemes (Seção 3.1):
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
Cada endpoint protegido referencia esse esquema com security: [{ bearerAuth: [] }] (como fizemos em POST /api/eventos, PUT /api/eventos/{id}, DELETE /api/eventos/{id} e todas as rotas de inscrições). O efeito no Swagger UI:
- Um cadeado aparece ao lado de cada operação protegida.
- Um botão verde "Authorize" aparece no topo da página.
- Clicar nele abre um campo para colar o token — só o token puro, sem o prefixo
Bearer(o Swagger UI adiciona isso sozinho no cabeçalhoAuthorization). - Depois de autorizado, todo "Try it out" em endpoint protegido já envia o cabeçalho automaticamente.
💡 Dica Para obter um token de teste rápido, abra o console do navegador na sua aplicação front-end já logada e rode:
js import { getAuth } from 'firebase/auth' const token = await getAuth().currentUser.getIdToken() console.log(token)Copie o valor impresso e cole no botão "Authorize" do Swagger UI.
8. Testando pelo Swagger UI ("Try it out")¶
- Abra
http://localhost:3000/api-docs. - Expanda
GET /api/eventos, clique em "Try it out", depois em "Execute" — a resposta real da API aparece embaixo, com status e corpo formatado. - Para testar
POST /api/eventos, clique em "Authorize" primeiro (Seção 7), depois expanda a operação, edite o JSON de exemplo no campo de corpo, e execute.
⚠️ Atenção — CORS e
serversO Swagger UI faz a requisição do navegador, então as mesmas regras de CORS da Aula 13 se aplicam: seserversapontar para uma URL diferente da que está rodando o front (ou se a API não liberar a origem da própria página do Swagger UI), o "Try it out" falha com erro de CORS no console — mesmo a API estando no ar. Garanta queCORS_ORIGEM_PERMITIDAinclua a origem de onde o Swagger UI está sendo servido (geralmente a própria API,http://localhost:3000, o que já é liberado por padrão pelo mesmo processo).
9. Além do Swagger: documentação completa do projeto¶
9.1 README de qualidade¶
<!-- README.md -->
# UniEventos API



API REST da plataforma **UniEventos** — divulgação e inscrição em eventos acadêmicos.
Projeto desenvolvido na disciplina FACET-SNP-310 (UNEMAT/Sinop, 2026.2).
## Requisitos
- Node.js 22 LTS
- MySQL 8 (local ou gerenciado)
- Conta de serviço do Firebase (arquivo de credenciais)
## Instalação
\`\`\`bash
git clone https://github.com/seu-usuario/unieventos-api.git
cd unieventos-api
npm install
cp .env.example .env # preencha com suas credenciais
npm run migrar
npm run dev
\`\`\`
## Variáveis de ambiente
| Variável | Descrição |
|---|---|
| `PORT` | Porta HTTP da API (padrão 3000) |
| `DB_HOST` / `DB_PORT` / `DB_USER` / `DB_PASSWORD` / `DB_NAME` | Credenciais do MySQL |
| `FIREBASE_PROJECT_ID` | Id do projeto Firebase usado na verificação de token |
| `CORS_ORIGEM_PERMITIDA` | Origem do front-end autorizada pelo CORS |
## Scripts disponíveis
| Comando | Efeito |
|---|---|
| `npm run dev` | Sobe a API com recarregamento automático |
| `npm start` | Sobe a API em modo produção |
| `npm test` | Executa a suíte de testes (vitest) |
| `npm run migrar` | Aplica migrations pendentes no banco |
## Endpoints
Documentação interativa completa em `/api-docs` (Swagger UI) com o projeto rodando.
Resumo:
| Método | Rota | Descrição |
|---|---|---|
| GET | `/api/eventos` | Lista eventos, com filtros |
| GET | `/api/eventos/:id` | Detalha um evento |
| POST | `/api/eventos` | Cria evento (autenticado) |
| PUT | `/api/eventos/:id` | Atualiza evento (autenticado) |
| DELETE | `/api/eventos/:id` | Remove evento (autenticado) |
| GET | `/api/inscricoes` | Lista inscrições do usuário logado |
| POST | `/api/inscricoes` | Inscreve o usuário em um evento |
| DELETE | `/api/inscricoes/:id` | Cancela inscrição |
## Licença
MIT — veja o arquivo LICENSE.
9.2 Coleção de API exportada¶
Além do Swagger UI, exporte uma coleção do Insomnia ou Postman e comite no repositório em docs/insomnia-collection.json — facilita quem prefere testar fora do navegador. No Insomnia: menu Application → Preferences → Data → Export Data, escolha a workspace do projeto, formato Insomnia v4, e salve o arquivo na pasta docs/ do repositório.
9.3 CONTRIBUTING.md mínimo¶
<!-- CONTRIBUTING.md -->
# Como contribuir
1. Crie uma branch a partir de `main`: `git checkout -b feature/nome-da-mudanca`.
2. Rode `npm test` antes de abrir o Pull Request — a suíte precisa passar.
3. Siga o padrão de nomes em português para identificadores de domínio (`eventos`, `criarEvento`).
4. Toda rota nova precisa ter anotação `@openapi` correspondente (Aula 14).
5. Abra o Pull Request descrevendo o que mudou e por quê.
9.4 Documentação do front: JSDoc em composables¶
// src/composables/useEventos.js
/**
* Composable que encapsula a busca e o estado de carregamento da lista de eventos.
*
* @param {Object} [opcoes] - opções de filtro inicial
* @param {string} [opcoes.categoria] - categoria para filtrar a busca inicial
* @returns {{
* eventos: import('vue').Ref<Array>,
* carregando: import('vue').Ref<boolean>,
* erro: import('vue').Ref<string|null>,
* buscarEventos: (filtros?: Object) => Promise<void>
* }}
*/
export function useEventos(opcoes = {}) {
// implementação já construída na Aula 06/11 — reaproveitada aqui
}
Comentários JSDoc em composables dão autocomplete e checagem de tipo básica no VS Code, mesmo em projetos JavaScript puro (sem TypeScript) — o editor lê o @param/@returns e sugere os campos corretos a quem consome o composable.
9.5 ADR — Architecture Decision Record¶
Um ADR é um documento curto (10 a 20 linhas) que registra uma decisão técnica, o contexto que levou a ela, e as alternativas consideradas — para que, meses depois, ninguém precise adivinhar "por que fizemos assim?".
Formato em 10 linhas:
# ADR 0001: <título curto da decisão>
**Status:** aceito | proposto | substituído por ADR-000X
## Contexto
<qual problema motivou esta decisão>
## Decisão
<o que foi decidido>
## Consequências
<o que fica mais fácil, o que fica mais difícil, o que foi trocado por quê>
Exemplo real do UniEventos:
<!-- docs/adr/0001-escolha-do-repository-pattern.md -->
# ADR 0001: Usar o padrão Repository para acesso a dados
**Status:** aceito
## Contexto
O service de eventos precisava consultar o MySQL diretamente, o que impedia
testar as regras de negócio (ex.: "vagas não pode ser negativo") sem subir
um banco de dados real, e acoplava o service à sintaxe SQL do mysql2.
## Decisão
Extrair toda a lógica de acesso a dados para `repositories/`, com uma
interface comum (`listar`, `buscarPorId`, `criar`, `atualizar`, `remover`),
injetada no service por parâmetro (Dependency Injection). O ambiente de
teste usa uma implementação em memória; produção usa a implementação MySQL.
## Consequências
Testes de service ficaram instantâneos e sem dependência externa. Trocar o
banco de dados (como fizemos ao avaliar Supabase na Aula 12) passou a exigir
apenas uma nova implementação de repositório, sem tocar em services ou
controllers. Custo: uma camada de indireção a mais para quem está lendo o
código pela primeira vez.
📌 Na prova Um ADR não documenta código — documenta decisão e motivo. Se a resposta para "por que você fez assim?" está só na sua cabeça, ela vai se perder. Escrever ADRs curtos ao longo do desenvolvimento é mais barato do que reconstruir esse raciocínio depois.
🧪 Laboratório¶
1. Configure swagger-jsdoc e swagger-ui-express no seu projeto autoral, com definition (não swaggerDefinition), info, pelo menos uma tag e o securityScheme bearerAuth.
Resultado esperado: http://localhost:3000/api-docs abre com o título e a descrição da sua API.
Dica
Copie src/docs/swaggerSpec.js da Seção 3.1 e troque só o title, description e as tags para o domínio do seu projeto.
2. Documente 3 endpoints do seu projeto autoral com anotações @openapi completas (parâmetros, requestBody quando houver, respostas para pelo menos 2 status diferentes).
Resultado esperado: os 3 endpoints aparecem expansíveis no Swagger UI, com exemplos de corpo preenchidos.
Dica
Comece pelo endpoint de listagem (mais simples, sem requestBody) e depois avance para um de criação (com requestBody e security).
3. Crie os schemas reutilizáveis da entidade principal do seu domínio (equivalente a Evento/EventoInput/Erro) e referencie com $ref nos 3 endpoints do exercício anterior.
Resultado esperado: mudar um campo no schema reflete automaticamente em todos os endpoints que o referenciam.
Dica
Coloque os schemas em src/docs/schemas/*.schema.js e inclua o caminho no array apis da configuração do swagger-jsdoc.
4. Teste um endpoint protegido pelo "Authorize" — obtenha um token do Firebase (Seção 7) e confirme que a requisição autenticada funciona pelo Swagger UI.
Resultado esperado: sem token, a rota protegida retorna 401; com token válido, retorna 200/201.
Dica
Se a resposta continuar 401 mesmo com token colado, confira se você colou só o token puro, sem o prefixo Bearer.
5. Escreva um ADR para uma decisão técnica real do seu projeto (ex.: por que escolheu MySQL ou Supabase, por que escolheu determinado padrão de rota).
Resultado esperado: arquivo docs/adr/0001-<slug>.md seguindo o formato de 10 linhas da Seção 9.5.
Dica
Escolha uma decisão que você realmente tomou e hesitou entre alternativas — é mais fácil escrever o "Contexto" quando a dúvida foi real.
🐛 Erros comuns e como resolver¶
| Sintoma | Causa | Solução |
|---|---|---|
/api-docs abre, mas paths está vazio |
Usou a chave swaggerDefinition em vez de definition nas opções do swagger-jsdoc |
Troque para definition — é a chave exigida na versão 6.x |
| Endpoint documentado não aparece no Swagger UI | O arquivo onde está o comentário @openapi não está listado em apis: [...] |
Adicione o caminho (ou glob) do arquivo à lista apis da configuração |
$ref: '#/components/schemas/Evento' gera erro "not found" |
O schema Evento não foi anotado em nenhum arquivo varrido pelo apis |
Confira se src/docs/schemas/evento.schema.js está no array apis e se o YAML do comentário está corretamente indentado |
| Botão "Authorize" não aparece | Nenhum endpoint tem security: [{ bearerAuth: [] }], ou securitySchemes não foi declarado em components |
Declare securitySchemes.bearerAuth em definition.components e adicione security nos endpoints protegidos |
| "Try it out" falha com erro de CORS | A origem da própria página do Swagger UI não está liberada pelo middleware de CORS da API | Garanta que a origem da API (onde o /api-docs é servido) está coberta pela configuração de CORS, ou sirva o Swagger UI na mesma origem da API |
YAML do comentário @openapi quebra a spec inteira silenciosamente |
Indentação incorreta no bloco YAML dentro do comentário JSDoc | YAML é sensível a espaços — nunca misture tabs, use 2 espaços por nível, consistentemente |
🏠 Atividade assíncrona (1 h)¶
- Documente todos os endpoints do seu projeto autoral com anotações
@openapi(não só os 3 do laboratório). - Garanta que os schemas
Erroe de paginação (se aplicável) estão presentes e referenciados. - Revise o
README.mdseguindo a estrutura da Seção 9.1: badges, requisitos, instalação, variáveis de ambiente, scripts, endpoints (com link para/api-docs), licença. - Escreva pelo menos 1 ADR adicional sobre uma decisão do seu back-end.
Critério de pronto: /api-docs mostra 100% dos endpoints do projeto autoral documentados; README revisado; ao menos 2 ADRs no repositório.
✅ Checkpoint do projeto autoral¶
Ao final desta aula, seu repositório <tema>-api deve ter:
- [ ]
swagger-jsdocconfigurado com a chavedefinitioneswagger-ui-expressservindo em/api-docs. - [ ]
/api-docs.jsonexpondo a spec crua. - [ ] Schemas reutilizáveis (
$ref) para a entidade principal, incluindo um schema deErro. - [ ]
securitySchemebearerAuthconfigurado e usado em todos os endpoints protegidos. - [ ] README revisado com badges, instalação, variáveis de ambiente, scripts e tabela de endpoints.
- [ ] Pasta
docs/adr/com pelo menos 2 registros de decisão.
📚 Para aprofundar¶
- Especificação OpenAPI 3.0 (oficial)
- swagger-jsdoc — repositório no GitHub
- swagger-ui-express — repositório no GitHub
- Swagger.io — guia oficial de OpenAPI
- ADR GitHub organization — modelos de Architecture Decision Record
- Keep a README — checklist do que compõe um bom README
Próxima aula (15, 16/12/2026): fechamos o semestre com deploy real (front e back), CI/CD com GitHub Actions, retrospectiva de todos os padrões de projeto usados, guia de estudo para o exame final e as instruções completas da Avaliação 3. Traga a API documentada e pronta para publicar.
Prof. Ivan Luiz Pedroso Pires · Material didático de uso educacional; livre para consulta, estudo e reuso com atribuição.
Início · Banco de Desafios · Links úteis · Fontes no GitHub