Boilerplate de back-end com Fastify + TypeScript + Drizzle ORM, com foco em base limpa, segura e pronta para evoluir.
- Fastify 5
- TypeScript strict
- Drizzle ORM + PostgreSQL
- TypeBox para validação de payload
- JWT com
@fastify/jwt - Jest para testes de integração
- Docker e Docker Compose
- GitHub Actions CI (lint, test, audit, docker)
src/
app.ts
server.ts
config/
env.ts
db/
connection.ts
schema.ts
migrations/
seed.ts
modules/
auth/
auth.controller.ts
auth.route.ts
auth.schema.ts
auth.service.ts
users/
user.service.ts
plugins/
drizzle.ts
errorHandler.ts
jwt.ts
rateLimit.ts
utils/
appError.ts
database.ts
httpStatusCodes.ts
response.ts
schemaErrorFormatter.ts
scripts/
run-tests.ts
test/
auth/
auth.test.ts
config/
env.test.ts
setup.ts
.github/
workflows/
ci.yml
Dockerfile
docker-compose.yml
docker-compose.development.yml
deploy.sh
run-db.sh
- Node.js 22+
- pnpm
- Docker (para testes, banco local e imagem de produção)
- PostgreSQL 16+ (via Docker incluído neste repositório ou externo)
Dúvidas e erros comuns: TROUBLESHOOTING.md.
O arquivo sample.env já contém valores coerentes entre si (DATABASE_URL alinhada com POSTGRES_* para uso em localhost:5432). Para começar:
pnpm installCopy-Item sample.env .env(PowerShell) oucp sample.env .env(Bash)- Subir só o Postgres em
127.0.0.1:5432:pnpm docker:db(aguarde alguns segundos) pnpm db:migrateepnpm dev
API em http://localhost:3000, documentação em http://localhost:3000/docs. Testes: pnpm test (sobe o Postgres via Docker automaticamente).
Stack completa no Docker (API + Postgres, banco não exposto ao host): com o mesmo .env, use pnpm docker:up. API + Postgres com ferramentas no host (Studio, psql): pnpm docker:up:dev.
| O que | Motivo |
|---|---|
JWT_SECRET |
Trocar o valor de exemplo por um segredo longo e aleatório (mínimo 32 caracteres). Nunca commitar o valor real. |
POSTGRES_PASSWORD e usuário / base |
Credenciais fortes em produção; alinhar com o que o Postgres da infraestrutura espera. |
CORS_ORIGIN |
Domínios reais do front-end (sem * em produção). |
TRUST_PROXY |
true se a API estiver atrás de Nginx, Cloudflare ou load balancer (rate limit e IP corretos). |
| Migrations em produção | Preferir job de CI/CD ou pnpm db:migrate antes do deploy; desativar migrations na subida do container no seu override, se adotar essa política. |
O passo a passo de Docker e rede está na seção Docker. Em resumo: a configuração é segura por padrão — o Postgres fica só na rede interna do Compose (pnpm docker:up); para desenvolvimento com o banco acessível na máquina, use pnpm docker:up:dev ou pnpm docker:db.
- Instale dependências:
pnpm install- Copie as variáveis:
cp sample.env .envPowerShell: Copy-Item sample.env .env
-
Garanta um Postgres acessível pela
DATABASE_URL(por exemplopnpm docker:dbcom osample.envsem alterações). -
Rode migrations:
pnpm db:migrate- Inicie em desenvolvimento:
pnpm devAPI em http://localhost:3000
Docs em http://localhost:3000/docs
JWT_SECRETprecisa ter no mínimo 32 caracteres fora detest.JWT_ISSUEReJWT_AUDIENCEsão validados na assinatura e verificação do token. Defaults:real-world-fastifyereal-world-fastify-users.- Em produção,
CORS_ORIGINdeve ser uma allowlist explícita (sem*). - Em produção,
ENABLE_DOCSé desabilitado por padrão. TRUST_PROXYdeve sertruequando a aplicação rodar atrás de reverse proxy (Nginx, Cloudflare, ALB). Afetarequest.ipe rate limit.BCRYPT_ROUNDSé configurável via env (default10). Aumentar em produção conforme capacidade do hardware.- No
docker-compose.yml,RUN_MIGRATIONS_ON_STARTUPeENABLE_DOCStêm valores fixos (trueefalse) para não serem sobrescritos por variáveis globais do sistema. Em deploy real, desative migrations na subida e rode-as em job dedicado; usedocker-compose.override.yml(ignorado pelo Git) se precisar ajustar o Compose localmente. - Emails são normalizados para lowercase na criação e busca de usuários.
- Login usa comparação timing-safe: tempo de resposta é constante independentemente de o email existir ou não, prevenindo enumeração de usuários por timing attack.
- Registro é race-condition safe: usa insert direto com captura de violação de unique constraint (409 Conflict), eliminando TOCTOU.
- Rate limit global aplicado a todas as rotas. Endpoints de auth (
/login,/register) possuem limites mais restritivos configurados por rota. - Content Security Policy (CSP) do Helmet é habilitada em produção (quando
ENABLE_DOCS=false). Em desenvolvimento, CSP é desabilitada para compatibilidade com Swagger UI. - O seed (
db:seed) possui guard contra execução em produção. - O endpoint
/meconsulta o banco para retornar dados atualizados do usuário, garantindo que tokens de usuários deletados sejam rejeitados.
| Variável | Obrigatória | Default | Descrição |
|---|---|---|---|
NODE_ENV |
Não | development |
development, test ou production |
PORT |
Não | 3000 |
Porta do servidor |
HOST |
Não | 0.0.0.0 |
Host de bind |
DATABASE_URL |
Sim | — | Connection string PostgreSQL |
JWT_SECRET |
Sim | — | Segredo JWT (min 32 chars fora de test) |
JWT_EXPIRES_IN |
Não | 1d |
Tempo de expiração do token |
JWT_ISSUER |
Não | real-world-fastify |
Issuer do JWT |
JWT_AUDIENCE |
Não | real-world-fastify-users |
Audience do JWT |
CORS_ORIGIN |
Não | * (dev) |
Origens permitidas (lista separada por vírgula) |
ENABLE_DOCS |
Não | true (dev) / false (prod) |
Habilita Swagger UI em /docs (no docker-compose.yml deste repositório o valor é false, fixo; no docker-compose.development.yml é true) |
RUN_MIGRATIONS_ON_STARTUP |
Não | true (dev) / false (prod) |
Roda migrations ao iniciar (no docker-compose.yml deste repositório o valor é true, fixo) |
TRUST_PROXY |
Não | false |
Habilita trust proxy no Fastify |
LOG_LEVEL |
Não | info |
Nível de log Pino |
BCRYPT_ROUNDS |
Não | 10 |
Rounds de hash bcrypt |
RATE_LIMIT_MAX |
Não | 100 |
Requisições por janela (global) |
RATE_LIMIT_WINDOW |
Não | 1 minute |
Janela de rate limit global |
RATE_LIMIT_AUTH_MAX |
Não | 5 |
Requisições por janela (auth) |
RATE_LIMIT_AUTH_WINDOW |
Não | 1 minute |
Janela de rate limit auth |
DB_POOL_MAX |
Não | 10 |
Tamanho máximo do pool de conexões PostgreSQL |
pnpm dev: desenvolvimento com watchpnpm build: build TypeScriptpnpm start: inicia build de produçãopnpm lint: lint do projetopnpm lint:fix: lint com autofixpnpm format: formata códigopnpm format:check: valida formataçãopnpm test: sobe container Postgres efêmero, aplica migrations, roda jest e limpa o container (requer Docker)pnpm test:watch: jest em modo watch (requer banco rodando)pnpm test:coverage: cobertura de testespnpm db:generate: gera migration Drizzlepnpm db:migrate: aplica migrationspnpm db:push: aplica schema sem migrationpnpm db:studio: abre Drizzle Studiopnpm db:seed: popula dados de exemplopnpm docker:up/pnpm docker:up:dev/pnpm docker:down/pnpm docker:db: stack segura (Compose base), stack com Postgres emlocalhost, encerrar stack, só o banco com porta local
O docker compose lê o arquivo .env na raiz do projeto para interpolar variáveis. Defina POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB e JWT_SECRET (mínimo 32 caracteres) no .env (pode copiar do sample.env). O serviço app recebe DATABASE_URL montada a partir desses dados com hostname interno db; não é necessário repetir DATABASE_URL no .env para a stack Docker.
Por padrão, o PostgreSQL não publica porta no host: apenas o container da API na rede app-network alcança o banco. O docker-compose.yml fixa ENABLE_DOCS=false e RUN_MIGRATIONS_ON_STARTUP=true no serviço app. O arquivo docker-compose.development.yml expõe 127.0.0.1:5432 no Postgres e define ENABLE_DOCS=true no app para um fluxo de desenvolvimento mais próximo do dia a dia.
As migrations rodam na subida do container neste Compose, por padrão. Em produção, desative migrations na subida (override ou orquestrador) e rode-as em job dedicado antes do deploy.
Banco apenas com porta local (para desenvolvimento com pnpm dev):
./deploy.sh --db-onlyAplicação completa sem expor o banco ao host (recomendado para produção / homologação segura):
./deploy.sh --buildMesma stack com PostgreSQL em 127.0.0.1:5432:
./deploy.sh --build --devEquivalente com pnpm: pnpm docker:up (seguro por padrão) e pnpm docker:up:dev (Postgres em localhost + Swagger habilitado no container).
Parar tudo:
./deploy.sh --downO script deploy.sh é Bash (Git Bash ou WSL no Windows). No PowerShell use os comandos pnpm docker:* acima.
Credenciais e JWT_SECRET ficam no .env. Senhas com caracteres especiais na URL do Postgres devem ser codificadas (percent-encoding) se você montar DATABASE_URL manualmente; no Compose, prefira senhas alfanuméricas nos campos POSTGRES_* ou consulte a documentação do PostgreSQL sobre URIs de conexão.
Para sobrescrever só o Compose localmente, crie docker-compose.override.yml (está no .gitignore).
Para executar migrations em produção, rode um job dedicado:
pnpm db:migrateA pipeline GitHub Actions (.github/workflows/ci.yml) roda em push e PR na branch master com jobs paralelos:
- lint:
pnpm lint+pnpm format:check - test: Postgres via service container,
pnpm db:migrate+jest - audit:
pnpm audit --prodpara vulnerabilidades em dependências - docker: validação de
docker compose config(stack segura por padrão e override de desenvolvimento) e build da imagem da aplicação
- Arquitetura por módulo (
route,controller,service,schema) - Respostas padronizadas via
utils/response.ts: sucesso retorna{ success, data }, erro retorna{ success, message, statusCode } - Erros de domínio com
AppError(mensagem exposta ao cliente apenas para status < 500) - Configuração centralizada e tipada em
config/env.ts - Type-safety sem
anynos pontos críticos - Emails normalizados para lowercase na camada de dados
- Graceful shutdown com tratamento de
SIGINT,SIGTERM,uncaughtExceptioneunhandledRejection trustProxyconfigurável via env para ambientes com reverse proxy
pnpm test é auto-suficiente: o script scripts/run-tests.ts sobe um container Postgres efêmero com porta aleatória, aplica migrations, executa jest e remove o container no final. Requer Docker rodando.
Os testes usam buildApp() e app.inject() sem subir servidor HTTP externo. O arquivo test/setup.ts define variáveis de ambiente para o ambiente de teste.
Para rodar jest manualmente contra um banco já existente:
DATABASE_URL=postgresql://... pnpm exec jestPowerShell:
$env:DATABASE_URL = "postgresql://usuario:senha@localhost:5432/banco"; pnpm exec jest