API REST production-ready com NestJS 11, Fastify, Prisma, JWT, RBAC e boas práticas.
| Camada | Tecnologia |
|---|---|
| Framework | NestJS 11 + Fastify 5 |
| Banco de dados | PostgreSQL 16 + Prisma ORM 6 |
| Autenticação | JWT (access + refresh com rotação) + argon2 |
| Autorização | RBAC (roles + permissions) + owner-or-permission guard |
| Validação | Zod 4 (env, body, query, params) |
| Documentação | Swagger (OpenAPI) |
| Segurança | Helmet, Rate Limit, CORS |
| Logging | Pino (structured JSON) |
| Deploy | Docker multi-stage + docker-compose |
| Testes | Jest + Supertest + testes integrados com banco real |
git clone <repo-url> my-api
cd my-api
npm installcp .env.example .envEdite o .env com seus valores (veja Variáveis de Ambiente).
docker compose up -dIsso sobe o PostgreSQL, aplica migrations, roda o seed e inicia a API automaticamente.
- API: http://localhost:3000
- Swagger: http://localhost:3000/docs
# Subir apenas o banco
docker compose up -d db
# Aplicar migrations e seed
npx prisma migrate dev
npm run prisma:seed
# Iniciar em modo dev (hot-reload)
npm run start:devApós o seed, um usuário admin é criado:
| Campo | Valor |
|---|---|
admin@template.com |
|
| Senha | Admin@123 |
| Role | admin (todas as permissions) |
# ── App ──
NODE_ENV=development
PORT=3000
# ── Database ──
DATABASE_URL=postgresql://template:template@localhost:5432/template
# ── JWT ──
JWT_ACCESS_SECRET=change-me-access-secret-min-32-chars!!
JWT_ACCESS_EXPIRES_IN=15m
JWT_REFRESH_SECRET=change-me-refresh-secret-min-32-chars!!
JWT_REFRESH_EXPIRES_IN=7d
# ── CORS ──
CORS_ORIGINS=http://localhost:3000,http://localhost:4200
# ── Crypto (opcional — AES-256-GCM) ──
# CRYPTO_KEY=your-64-char-hex-key-here
# CRYPTO_IV_LENGTH=16Todas as variáveis são validadas com Zod na inicialização. A app não sobe se alguma obrigatória estiver faltando.
| Comando | Descrição |
|---|---|
npm run start:dev |
Inicia em modo dev com hot-reload |
npm run start:prod |
Inicia em modo produção (node dist/src/main) |
npm run build |
Compila o projeto |
npm run lint |
Roda ESLint com auto-fix |
npm run format |
Formata com Prettier |
npm test |
Testes unitários |
npm run test:e2e |
Testes e2e |
npm run test:cov |
Testes com coverage |
npm run prisma:migrate |
Cria nova migration |
npm run prisma:deploy |
Aplica migrations pendentes |
npm run prisma:seed |
Roda seed (roles, permissions, admin) |
npm run prisma:studio |
Abre Prisma Studio (GUI do banco) |
npm run prisma:generate |
Regenera o Prisma Client |
src/
├── common/
│ ├── constants/ # ErrorCode enum
│ ├── decorators/ # @CurrentUser, @Roles, @Permissions
│ ├── filters/ # GlobalExceptionFilter (Prisma, Zod, HTTP)
│ ├── guards/ # JwtAuth, UserStatus, Roles, Permissions, OwnerOrPermission
│ ├── pipes/ # ZodBodyPipe, ZodQueryPipe, ZodParamsPipe
│ └── utils/ # pagination (cursor + offset), crypto (AES-256-GCM)
├── config/ # Validação de env e config objects (app, db, jwt, cors, crypto)
├── modules/
│ ├── auth/ # Register, Login, Refresh, Logout
│ ├── health/ # GET /health, GET /health/db
│ └── user/ # CRUD de usuários com RBAC
├── prisma/ # PrismaModule, PrismaService (global)
├── app.module.ts
└── main.ts
prisma/
├── schema.prisma # Modelos: User, Role, Permission, UserRole, RolePermission, RefreshToken
├── seed.ts # Seed: permissions, roles, admin user
└── migrations/
test/
├── integration/ # Testes integrados com banco real (33 testes)
└── health.e2e-spec.ts # Teste e2e do health check
| Rota | Método | Auth | Descrição |
|---|---|---|---|
/health |
GET | Nenhuma | Status da aplicação |
/health/db |
GET | Nenhuma | Status da conexão com o banco |
curl http://localhost:3000/health
# {"status":"ok","timestamp":"...","uptime":123.45}
curl http://localhost:3000/health/db
# {"status":"ok","database":"connected","timestamp":"..."}| Rota | Método | Auth | Descrição |
|---|---|---|---|
/auth/register |
POST | Nenhuma | Registrar novo usuário |
/auth/login |
POST | Nenhuma | Login com email/senha |
/auth/refresh |
POST | Nenhuma | Renovar tokens (rotação) |
/auth/logout |
POST | Bearer Token | Revogar refresh token |
Cria um novo usuário com a role user e retorna os tokens JWT.
curl -X POST http://localhost:3000/auth/register \
-H 'Content-Type: application/json' \
-d '{
"email": "joao@example.com",
"password": "MinhaSenh@123",
"name": "João Silva"
}'Resposta (201):
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "a1b2c3d4e5f6...",
"expiresIn": "15m"
}Erros:
400— Validação falhou (email inválido, senha < 8 caracteres)409— Email já registrado
Autentica com email e senha. Retorna par de tokens.
curl -X POST http://localhost:3000/auth/login \
-H 'Content-Type: application/json' \
-d '{
"email": "admin@template.com",
"password": "Admin@123"
}'Resposta (200):
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "a1b2c3d4e5f6...",
"expiresIn": "15m"
}Erros:
401— Credenciais inválidas, conta inativa ou bloqueada
Renova o access token usando o refresh token. Implementa rotação de tokens: o refresh token antigo é revogado e um novo é gerado.
curl -X POST http://localhost:3000/auth/refresh \
-H 'Content-Type: application/json' \
-d '{
"refreshToken": "a1b2c3d4e5f6..."
}'Resposta (200):
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...(novo)",
"refreshToken": "x9y8z7w6v5u4...(novo)",
"expiresIn": "15m"
}Erros:
401— Refresh token inválido, expirado ou já revogado
Revoga o refresh token. Requer autenticação.
curl -X POST http://localhost:3000/auth/logout \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <accessToken>' \
-d '{
"refreshToken": "a1b2c3d4e5f6..."
}'Resposta (200): corpo vazio
Erros:
401— Não autenticado
Todas as rotas de /users requerem autenticação (Bearer Token).
| Rota | Método | Auth | Permissão | Descrição |
|---|---|---|---|---|
/users |
GET | Bearer | Role admin |
Listar todos (cursor pagination) |
/users/me |
GET | Bearer | Qualquer user | Perfil do usuário logado |
/users/:id |
GET | Bearer | Owner ou user:read |
Buscar por ID |
/users/:id |
PATCH | Bearer | Owner ou user:write |
Atualizar usuário |
/users/:id |
DELETE | Bearer | Role admin |
Soft delete (bloquear) |
Lista todos os usuários com paginação por cursor. Apenas admins.
curl http://localhost:3000/users \
-H 'Authorization: Bearer <adminAccessToken>'Resposta (200):
{
"data": [
{
"id": "cuid...",
"email": "admin@template.com",
"name": "Admin",
"isActive": true,
"isVerified": true,
"blockedAt": null,
"lastLoginAt": "2026-02-14T03:00:00.000Z",
"createdAt": "2026-02-14T02:00:00.000Z",
"updatedAt": "2026-02-14T03:00:00.000Z",
"roles": [
{
"userId": "cuid...",
"roleId": "cuid...",
"role": {
"id": "cuid...",
"name": "admin",
"description": "Full access administrator",
"permissions": [
{ "permission": { "name": "user:read" } },
{ "permission": { "name": "user:write" } },
{ "permission": { "name": "user:delete" } },
{ "permission": { "name": "role:read" } },
{ "permission": { "name": "role:write" } }
]
}
}
]
}
],
"meta": {
"nextCursor": "cuid...",
"hasNext": false
}
}Paginação por cursor:
# Primeira página (5 itens)
curl 'http://localhost:3000/users?take=5' \
-H 'Authorization: Bearer <adminAccessToken>'
# Próxima página
curl 'http://localhost:3000/users?take=5&cursor=<nextCursor>' \
-H 'Authorization: Bearer <adminAccessToken>'Erros:
401— Não autenticado403— Não é admin (INSUFFICIENT_ROLES)
Retorna o perfil do usuário logado. Qualquer usuário autenticado.
curl http://localhost:3000/users/me \
-H 'Authorization: Bearer <accessToken>'Resposta (200): objeto do usuário com roles e permissions (mesmo formato do /users).
Erros:
401— Não autenticado
Busca um usuário por ID. Permitido se:
- O usuário é o dono do recurso (mesmo ID), ou
- O usuário tem a permission
user:read
curl http://localhost:3000/users/cuid123abc \
-H 'Authorization: Bearer <accessToken>'Resposta (200): objeto do usuário.
Erros:
401— Não autenticado403— Sem permissão (INSUFFICIENT_PERMISSIONS)404— Usuário não encontrado
Atualiza dados do usuário. Permitido se:
- O usuário é o dono do recurso, ou
- O usuário tem a permission
user:write
curl -X PATCH http://localhost:3000/users/cuid123abc \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <accessToken>' \
-d '{
"name": "Novo Nome",
"email": "novo@email.com"
}'Todos os campos são opcionais. Envie apenas o que deseja alterar.
Resposta (200): objeto do usuário atualizado.
Erros:
400— Validação falhou (email inválido)401— Não autenticado403— Sem permissão404— Usuário não encontrado
Soft delete: marca o usuário como isActive: false e blockedAt: <agora>. Apenas admins.
curl -X DELETE http://localhost:3000/users/cuid123abc \
-H 'Authorization: Bearer <adminAccessToken>'Resposta (200): objeto do usuário com isActive: false e blockedAt preenchido.
O usuário bloqueado não consegue mais acessar rotas protegidas (retorna 403 ACCOUNT_BLOCKED).
Erros:
401— Não autenticado403— Não é admin404— Usuário não encontrado
Register/Login → { accessToken (15min), refreshToken (7d) }
│ │
Usar no header Usar para renovar
Authorization: Bearer POST /auth/refresh
│ │
Acessa rotas Gera novo par de tokens
protegidas (token antigo é revogado)
As rotas protegidas passam por até 4 guards em sequência:
- JwtAuthGuard — Valida o access token JWT
- UserStatusGuard — Verifica se o usuário está ativo e não bloqueado
- RolesGuard — Verifica se o usuário tem a role necessária (ex:
admin) - OwnerOrPermissionGuard — Verifica se o usuário é dono do recurso OU tem a permission necessária
| Role | Permissions |
|---|---|
admin |
user:read, user:write, user:delete, role:read, role:write |
user |
user:read |
Novos usuários registrados recebem automaticamente a role user.
Todas as respostas de erro seguem o mesmo formato:
{
"code": "VALIDATION_ERROR",
"message": "Body validation failed",
"details": [
{ "field": "email", "message": "Invalid email" }
],
"requestId": "uuid-da-request"
}| Código | HTTP | Descrição |
|---|---|---|
VALIDATION_ERROR |
400 | Validação Zod falhou |
BAD_REQUEST |
400 | Request inválida |
INVALID_CREDENTIALS |
401 | Email/senha incorretos |
TOKEN_INVALID |
401 | JWT inválido ou expirado |
REFRESH_TOKEN_REVOKED |
401 | Refresh token revogado |
FORBIDDEN |
403 | Acesso negado |
ACCOUNT_INACTIVE |
403 | Conta inativa |
ACCOUNT_BLOCKED |
403 | Conta bloqueada |
INSUFFICIENT_ROLES |
403 | Role insuficiente |
INSUFFICIENT_PERMISSIONS |
403 | Permission insuficiente |
NOT_FOUND |
404 | Recurso não encontrado |
CONFLICT |
409 | Recurso já existe |
INTERNAL_ERROR |
500 | Erro interno |
Usado em GET /users. Ideal para feeds e listas infinitas.
# Primeira página
GET /users?take=20
# Próxima página
GET /users?take=20&cursor=<nextCursor>Resposta:
{
"data": [...],
"meta": {
"nextCursor": "cuid...",
"hasNext": true
}
}take— Itens por página (1-100, padrão: 20)cursor— ID do último item da página anteriorhasNext— Se há mais itensnextCursor— Cursor para a próxima página (nullse não há mais)
docker compose up -dO container da app executa automaticamente:
prisma migrate deploy— Aplica migrations pendentesprisma db seed— Roda o seed (idempotente com upsert)node dist/src/main— Inicia a aplicação
docker compose up -d dbdocker build -t template-nestjs .O compose define dois serviços:
- db — PostgreSQL 16 Alpine (porta 5432)
- app — API NestJS (porta 3000, depende do db healthy)
npm testnpm run test:e2eOs testes integrados conectam ao PostgreSQL real e testam todas as rotas com autenticação, autorização e banco de dados.
Setup (uma vez):
# Subir o banco
docker compose up -d db
# Criar banco de teste
docker compose exec db psql -U template -c "CREATE DATABASE template_test"
# Aplicar migrations e seed
DATABASE_URL="postgresql://template:template@localhost:5432/template_test" npx prisma migrate deploy
DATABASE_URL="postgresql://template:template@localhost:5432/template_test" npx prisma db seedRodar:
DATABASE_URL="postgresql://template:template@localhost:5432/template_test" \
npx jest --config ./test/jest-e2e.config.js --testPathPatterns integration --verbose --forceExitCobertura (33 testes):
| Grupo | Testes | O que testa |
|---|---|---|
| POST /auth/register | 4 | Registro, email duplicado, validação |
| POST /auth/login | 4 | Login, credenciais inválidas |
| POST /auth/refresh | 2 | Rotação de tokens, token inválido |
| GET /users/me | 3 | Perfil, sem auth, token inválido |
| GET /users | 4 | Lista admin, paginação, non-admin rejeitado |
| GET /users/:id | 4 | Owner, admin, permission, 404 |
| PATCH /users/:id | 5 | Owner update, admin update, validação, 404 |
| DELETE /users/:id | 3 | Non-admin rejeitado, soft delete, 404 |
| POST /auth/logout | 3 | Logout, refresh rejeitado, sem auth |
| UserStatusGuard | 1 | Usuário bloqueado rejeitado |
User ──< UserRole >── Role ──< RolePermission >── Permission
│
└──< RefreshToken
| Modelo | Descrição |
|---|---|
| User | Usuário com email, senha (argon2), status, timestamps |
| Role | Papel (admin, user) |
| Permission | Permissão granular (user:read, user:write, etc.) |
| UserRole | Relação N:N entre User e Role |
| RolePermission | Relação N:N entre Role e Permission |
| RefreshToken | Tokens de refresh com expiração e revogação |
O seed cria:
- 5 permissions:
user:read,user:write,user:delete,role:read,role:write - 2 roles:
admin(todas as permissions),user(apenasuser:read) - 1 admin:
admin@template.com/Admin@123
O seed usa upsert, então é seguro rodar múltiplas vezes.
- Helmet — Headers de segurança HTTP
- Rate Limit — 100 requests/minuto por IP
- CORS — Origens configuráveis via env
- argon2 — Hash de senhas (resistente a GPU)
- JWT com rotação — Refresh tokens são revogados após uso
- Soft delete — Usuários bloqueados não são removidos do banco
- Validação Zod — Todas as entradas são validadas
- Pino logger — Logs estruturados com redação de dados sensíveis
Acesse http://localhost:3000/docs para a documentação interativa da API.
Para autenticar no Swagger:
- Faça login via
POST /auth/login - Copie o
accessTokenda resposta - Clique em Authorize no topo da página
- Cole o token no campo
Bearer <token> - Agora todas as rotas protegidas funcionam
Sham Vinicius Fiorin