Nível 3Unidade 3 · Integração front-end/back-end3 aulas de 50 min + 1 h EAD

Aula 14 — Documentação com Swagger

Nível 3 — Frameworks Modernos · FACET-SNP-310 · WebLab · Prof. Ivan Luiz Pedroso Pires

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

📋 Pré-requisitos desta aula

Checklist antes de começar:

🗺️ 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:

⚠️ 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-jsdoc resolve — 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

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:

YAML
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:

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.

Terminal
npm install swagger-jsdoc swagger-ui-express
JavaScript
// 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 definition dentro de opcoes. Em versões antigas do swagger-jsdoc (2.x/3.x) essa chave se chamava swaggerDefinition. Nesta disciplina usamos swagger-jsdoc@6.3.0, que exige definition. Se você copiar um tutorial antigo da internet com swaggerDefinition, a spec gerada fica com paths: {} 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:

YAML
# 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
JavaScript
// 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

JavaScript
// 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)
})
Terminal
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 mesmo app.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 @openapi fazem 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 do router.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.

JavaScript
// 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
JavaScript
// 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.js não exportam nada útil em termos de código JavaScript — servem só para o swagger-jsdoc encontrar o comentário (por isso estão incluídos em apis: [...] 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)

JavaScript
// 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

JavaScript
// 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:

JavaScript
// 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):

YAML
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:

  1. Um cadeado aparece ao lado de cada operação protegida.
  2. Um botão verde "Authorize" aparece no topo da página.
  3. 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çalho Authorization).
  4. 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")

  1. Abra http://localhost:3000/api-docs.
  2. Expanda GET /api/eventos, clique em "Try it out", depois em "Execute" — a resposta real da API aparece embaixo, com status e corpo formatado.
  3. 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 servers O Swagger UI faz a requisição do navegador, então as mesmas regras de CORS da Aula 13 se aplicam: se servers apontar 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 que CORS_ORIGEM_PERMITIDA inclua 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

Markdown
<!-- README.md -->
# UniEventos API

![Node.js](https://img.shields.io/badge/node-22.x-green)
![Express](https://img.shields.io/badge/express-5.2.1-blue)
![Licença](https://img.shields.io/badge/licença-MIT-lightgrey)

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

Markdown
<!-- 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

JavaScript
// 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:

Markdown
# 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:

Markdown
<!-- 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)

  1. Documente todos os endpoints do seu projeto autoral com anotações @openapi (não só os 3 do laboratório).
  2. Garanta que os schemas Erro e de paginação (se aplicável) estão presentes e referenciados.
  3. Revise o README.md seguindo a estrutura da Seção 9.1: badges, requisitos, instalação, variáveis de ambiente, scripts, endpoints (com link para /api-docs), licença.
  4. 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:

📚 Para aprofundar


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.

WebLab — Laboratório de Desenvolvimento Web · UNEMAT — Universidade do Estado de Mato Grosso · Campus Sinop · FACET
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