API completa em Node.js + TypeScript usando Fastify, Drizzle ORM (PostgreSQL), autenticação JWT e validação com Zod. Inclui documentação Swagger, testes automatizados e cobertura de código.
- Node.js 22+
- Docker e Docker Compose
- npm (ou outro gerenciador, mas o projeto usa
package-lock.json)
- Fastify 5 - Framework web rápido e eficiente para Node.js
- TypeScript - Tipagem estática para JavaScript
- fastify-type-provider-zod - Integração entre Fastify e Zod para validação tipada
- Drizzle ORM - ORM moderno e type-safe para TypeScript
- PostgreSQL - Banco de dados relacional
- pg - Driver PostgreSQL para Node.js
- Zod - Biblioteca de validação e parsing de schemas TypeScript-first
- jsonwebtoken - Implementação de JSON Web Tokens para autenticação
- argon2 - Algoritmo de hash seguro para senhas
- @fastify/swagger - Geração automática de documentação OpenAPI/Swagger
- @fastify/swagger-ui - Interface web para documentação da API
- Vitest - Framework de testes rápido e moderno
- @vitest/coverage-v8 - Cobertura de código usando V8
- Supertest - Biblioteca para testes de APIs HTTP
- @faker-js/faker - Geração de dados fake para testes e seed
- pino-pretty - Formatação de logs legíveis
- dotenv-cli - Carregamento de variáveis de ambiente
- Clone o repositório e acesse a pasta do projeto.
- Instale as dependências:
npm install- Suba os bancos PostgreSQL (desenvolvimento e teste) com Docker:
docker compose up -d- Rode as migrações (Drizzle):
npm run db:migrate- (Opcional) Popule o banco com dados de exemplo:
npm run db:seed(Opcional) Para inspecionar o schema/estado com o Drizzle Studio:
npm run db:studionpm run dev- Porta padrão:
http://localhost:3333 - Logs legíveis habilitados
- Documentação da API (em dev):
http://localhost:3333/docs
Base URL: http://localhost:3333
- POST
/sessions- Login do usuário
- Body (JSON):
{ "email": "user@example.com", "password": "12345aA@" } - Respostas:
- 200:
{ "token": "jwt-token" } - 400:
{ "message": "Credenciais inválidas." }
- 200:
-
POST
/courses🔒 Manager only- Cria um curso
- Headers:
Authorization: jwt-token - Body (JSON):
{ "title": "Curso de Docker" } - Respostas:
- 201:
{ "courseId": "<uuid>" }
- 201:
-
GET
/courses🔒 Authenticated users- Lista todos os cursos com paginação e busca
- Headers:
Authorization: jwt-token - Query params:
search,orderBy,page - 200:
{ "courses": [{ "id": "<uuid>", "title": "...", "enrollments": 5 }], "total": 10 }
-
GET
/courses/:id🔒 Authenticated users- Busca um curso pelo ID
- Headers:
Authorization: jwt-token - Parâmetros:
id(UUID) - Respostas:
- 200:
{ "course": { "id": "<uuid>", "title": "...", "description": "... | null" } } - 404: vazio
- 200:
-
PATCH
/courses/:id🔒 Manager only- Atualiza um curso existente
- Headers:
Authorization: jwt-token - Parâmetros:
id(UUID) - Body (JSON):
{ "title": "Novo título", "description": "Nova descrição" } - Respostas:
- 200:
{ "course": { "id": "<uuid>", "title": "...", "description": "..." } } - 404:
{ "error": "Curso não encontrado." }
- 200:
-
DELETE
/courses/:id🔒 Manager only- Remove um curso existente
- Headers:
Authorization: jwt-token - Parâmetros:
id(UUID) - Respostas:
- 200:
{ "deleted": "Curso deletado com sucesso." } - 404:
{ "error": "Curso não encontrado." }
- 200:
Há um arquivo request.http com exemplos prontos (compatível com extensões de REST Client).
Tabelas principais definidas em src/database/schema.ts:
-
users
id(uuid, pk, default random)name(text, obrigatório)email(text, único, obrigatório)password(text, obrigatório, hash argon2)role(enum: "student" | "manager", default "student")
-
courses
id(uuid, pk, default random)title(text, único, obrigatório)description(text, opcional)
-
enrollments
id(uuid, pk, default random)userId(uuid, fk para users.id)courseId(uuid, fk para courses.id)createdAt(timestamp, default now)
Cobertura geral: 97.2% das linhas cobertas
sequenceDiagram
participant C as Client
participant S as Fastify Server
participant JWT as JWT Middleware
participant V as Zod Validator
participant DB as Drizzle + PostgreSQL
%% Login Flow
C->>S: POST /sessions {email, password}
S->>V: Validar body
V-->>S: OK ou Erro 400
alt válido
S->>DB: SELECT user WHERE email=...
DB-->>S: user data
S->>S: Verificar senha (argon2)
alt senha correta
S->>S: Gerar JWT token
S-->>C: 200 {token}
else senha incorreta
S-->>C: 400 {message: "Credenciais inválidas"}
end
else inválido
S-->>C: 400
end
%% Manager-only Course Operations
C->>S: POST /courses {title} + Authorization header
S->>JWT: Verificar token JWT
JWT-->>S: OK (user data) ou 401
S->>S: Verificar role (manager)
alt autorizado
S->>V: Validar body
V-->>S: OK ou Erro 400
alt válido
S->>DB: INSERT INTO courses (title)
DB-->>S: {id}
S-->>C: 201 {courseId}
else inválido
S-->>C: 400
end
else não autorizado
S-->>C: 401
end
C->>S: GET /courses + Authorization header
S->>JWT: Verificar token JWT
JWT-->>S: OK (user data) ou 401
S->>S: Verificar role (manager)
alt autorizado
S->>DB: SELECT courses com enrollments count
DB-->>S: lista paginada
S-->>C: 200 {courses: [...], total}
else não autorizado
S-->>C: 401
end
%% Authenticated User Operations
C->>S: GET /courses/:id + Authorization header
S->>JWT: Verificar token JWT
JWT-->>S: OK (user data) ou 401
alt autenticado
S->>V: Validar param id (uuid)
V-->>S: OK ou Erro 400
alt encontrado
S->>DB: SELECT * FROM courses WHERE id=...
DB-->>S: course
S-->>C: 200 {course}
else não encontrado
S-->>C: 404
end
else não autenticado
S-->>C: 401
end
%% Manager-only Update/Delete Operations
C->>S: PATCH /courses/:id {title, description} + Authorization
S->>JWT: Verificar token JWT
JWT-->>S: OK (user data) ou 401
S->>S: Verificar role (manager)
S->>V: Validar param id (uuid) e body
V-->>S: OK ou Erro 400
alt válido e encontrado
S->>DB: UPDATE courses SET ... WHERE id=...
DB-->>S: updated course
S-->>C: 200 {course}
else inválido
S-->>C: 400
else não encontrado
S-->>C: 404
end
C->>S: DELETE /courses/:id + Authorization
S->>JWT: Verificar token JWT
JWT-->>S: OK (user data) ou 401
S->>S: Verificar role (manager)
S->>V: Validar param id (uuid)
V-->>S: OK ou Erro 400
alt válido e encontrado
S->>DB: DELETE FROM courses WHERE id=...
DB-->>S: OK
S-->>C: 200 {deleted: "..."}
else inválido
S-->>C: 400
else não encontrado
S-->>C: 404
end
npm run dev: inicia o servidor com reload e carrega variáveis de.envnpm run db:generate: gera artefatos do Drizzle a partir do schemanpm run db:migrate: aplica migrações no banconpm run db:studio: abre o Drizzle Studionpm run db:seed: popula o banco com dados de exemplo usando Fakernpm run test: executa testes E2E com cobertura de códigonpm run pretest: executa migrações no banco de teste antes dos testes
O projeto utiliza Vitest como framework de testes E2E com as seguintes características:
- Testes end-to-end para todas as rotas da API
- Cobertura de código com @vitest/coverage-v8
- Dados fake gerados com @faker-js/faker
- Banco de dados separado para testes (
courseApiTestna porta 5433) - Factory functions para criação de usuários e cursos de teste
- Testes de autenticação e autorização completos
- Simulação de requisições HTTP reais com Supertest
Para executar os testes:
npm run test- Conexão recusada ao Postgres: confirme
docker compose up -de que as portas5432e5433não estão em uso. - Variável
DATABASE_URLausente: verifique seu.env. O Drizzle exige essa variável paradb:generate,db:migrateedb:studio. - Erro de JWT: verifique se
JWT_SECRETestá definido no.env. - Testes falhando: certifique-se de que o banco de teste está rodando na porta
5433.
ISC (ver package.json).