Olá, comunidade tech! 👋
Compartilho um projeto que desenvolvi para dominar conceitos avançados como escalabilidade, observabilidade e DevOps — habilidades críticas para ambientes de alto volume. O objetivo era criar uma estrutura pronta para produção, mesmo em cenários simples, usando ferramentas adotadas por grande grande parte da comunidade.
🛠️ O que você vai encontrar nesse projeto?
Esse projeto combina ferramentas que são muito usadas no mercado e ajudam tanto no desenvolvimento quanto na manutenção:
- NestJS → Framework Node.js com padrão modular e testável
- Knex.js → Para lidar com banco de dados de forma flexível
- PostgreSQL → Banco de dados relacional
- Redis → Usado como cache para performance
- Docker + Docker Compose → Para rodar tudo localmente de forma padronizada
- Traefik → Balanceamento de carga entre múltiplas instâncias
- OpenTelemetry + Jaeger → Para rastrear o caminho de cada requisição na aplicação
🧠 Como o projeto funciona?
Essa aplicação permite cadastrar e listar heróis fictícios — algo simples. Mas o principal foco está em como toda a estrutura foi montada para ser observável, leve e pronta para escalar.
- A aplicação roda em mais de uma instância ao mesmo tempo
- O Traefik divide as requisições entre essas instâncias
- Cada requisição é monitorada desde o início até a resposta final
- Todos os dados do rastreamento podem ser vistos em uma interface chamada Jaeger
🧪 Ambiente de Desenvolvimento com Docker
Para facilitar o desenvolvimento local com hot reload e dependências controladas, usei um Dockerfile simples e direto, feito para rodar a aplicação NestJS dentro de um container com suporte ao modo de desenvolvimento:
# Etapa única: Ambiente de desenvolvimento FROM node:20-alpine # Define a pasta de trabalho dentro do container WORKDIR /app # Ativa o Corepack e configura o pnpm RUN corepack enable && corepack prepare pnpm@latest --activate # Copia os arquivos de dependências primeiro para aproveitar cache COPY pnpm-lock.yaml* package.json* ./ # Instala as dependências do projeto RUN pnpm install # Copia o restante da aplicação COPY . . # Expõe a porta padrão da aplicação NestJS EXPOSE 3000 # Inicia a aplicação em modo desenvolvimento com hot reload CMD ["pnpm", "start:dev"]
Esse arquivo foi feito com foco em:
✅ Facilidade de uso: com apenas um comando (docker compose up
), já é possível começar a desenvolver.
✅ Padronização do ambiente: todo o time trabalha com a mesma versão de Node, pnpm e dependências.
✅ Hot reload automático: ao salvar um arquivo, o servidor reinicia sozinho.
✅ Performance: base em Alpine Linux e uso de pnpm
, que é leve e rápido.
Essa abordagem é ideal para quem quer começar a usar Docker no dia a dia sem complicação, mas ainda sim mantendo boas práticas como uso de cache e estrutura limpa.
🐳 Multi-Stage Builds
Usei um Dockerfile com duas etapas para garantir que o ambiente de produção só tenha o necessário. Isso deixa a imagem menor, mais rápida e segura:
# Etapa 1: Construção com todas as dependências de desenvolvimento FROM node:20-alpine AS builder WORKDIR /app COPY pnpm-lock.yaml package.json ./ RUN corepack enable && corepack prepare pnpm@latest --activate && \ pnpm install --frozen-lockfile COPY . . RUN pnpm build # Etapa 2: Imagem final apenas com o necessário FROM node:20-alpine AS production WORKDIR /app ENV NODE_ENV=production COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules EXPOSE 3000
Esse modelo separa a fase de desenvolvimento da produção e é muito usado em ambientes reais.
Claro! Aqui está uma seção do post que explica, de forma simples e objetiva, o que está sendo feito nesse trecho de código TypeScript relacionado à instrumentação e observabilidade com OpenTelemetry, mantendo o mesmo estilo do restante do post:
🔍 Instrumentação Automática com OpenTelemetry
Para rastrear as requisições de forma automática, configurei o OpenTelemetry diretamente na aplicação, permitindo capturar informações valiosas sem precisar instrumentar manualmente cada trecho do código.
Abaixo está a configuração feita no projeto:
import { FastifyOtelInstrumentation } from '@fastify/otel'; import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import { NodeSDK } from '@opentelemetry/sdk-node'; const sdk = new NodeSDK({ serviceName: 'heroes-api', traceExporter: new OTLPTraceExporter({ url: process.env.TRACE_EXPORTER_URL, }), instrumentations: [ getNodeAutoInstrumentations(), new HttpInstrumentation(), new FastifyOtelInstrumentation({ servername: 'fastify-heroes-api', registerOnInitialization: true, }), ], });
🧠 O que está acontecendo aqui?
-
NodeSDK
: Inicializa o SDK principal do OpenTelemetry no Node.js. -
traceExporter
: Define o destino dos dados de rastreamento. Neste caso, eles são enviados via HTTP para o coletor (Jaeger). -
instrumentations
: Aqui ativamos três instrumentações:- Auto Instrumentations: Captura automaticamente métricas de bibliotecas populares como Express, pg, etc.
- HTTP Instrumentation: Foca especificamente em requisições HTTP.
- Fastify Instrumentation: Adiciona suporte específico para o framework Fastify usado pelo NestJS internamente.
🛑 Encerramento com segurança
process.on('SIGTERM', () => { sdk.shutdown().then(() => { console.log('SDK shut down successfully'); process.exit(0); }); });
Esse trecho garante que, ao encerrar a aplicação, o SDK finalize corretamente as tarefas e envie todos os dados de tracing antes de desligar — prática importante em ambientes reais.
🧭 Inicialização do Tracing
export const initTrace = () => { sdk.start(); };
Por fim, exportamos a função initTrace
para que o tracing possa ser iniciado assim que a aplicação subir, no caso dessa aplicação, sera iniciado dentro da função bootstrap()
no arquivo main.js
.
Com essa configuração, cada requisição que passa pela API é automaticamente monitorada, criando spans que podem ser visualizados na interface do Jaeger. Isso permite entender melhor o comportamento da aplicação, identificar gargalos e observar a relação entre os serviços.
Essa é uma das formas mais poderosas de trazer observabilidade para dentro da sua stack sem adicionar complexidade no código principal da aplicação.
⚙️ Orquestração Local com Docker Compose + Traefik
Para simular um ambiente de produção diretamente no seu ambiente de desenvolvimento, usei o Docker Compose com múltiplos serviços rodando em containers. Essa abordagem permite testar balanceamento de carga, limites de recursos e observabilidade de forma realista — tudo localmente.
Abaixo está uma parte do docker-compose.yml
com os principais serviços:
🧱 app
e app2
: instâncias da aplicação
app: &app build: dockerfile: Dockerfile.dev container_name: app1 volumes: - .:/app - /app/node_modules ports: - 3000:3000 mem_limit: 1g cpus: 0.5 depends_on: - db - redis environment: - NODE_ENV=development - DATABASE_URL=postgresql://root:root@db:5432/app?schema=fyoussef - REDIS_URL=redis://redis:6379 - TRACE_EXPORTER_URL=http://otel-collector:4318/v1/traces networks: - fyoussef labels: - 'traefik.enable=true' - 'traefik.http.routers.app.rule=Host(`localhost`)' - 'traefik.http.routers.app.entrypoints=web' - 'traefik.http.services.app.loadbalancer.server.port=3000' app2: <<: *app container_name: app2 ports: - 3001:3000
O que está sendo feito aqui:
-
app
eapp2
são duas instâncias da mesma aplicação, permitindo testar escalabilidade horizontal e load balancing com o Traefik. - Volumes: o código local é montado no container (
.:/app
), garantindo atualizações em tempo real. Onode_modules
local é ignorado para evitar conflitos. - Recursos controlados: limitamos memória (
1g
) e CPU (0.5
), permitindo estudar o comportamento sob diferentes condições de carga. - Variáveis de ambiente definem conexões com PostgreSQL, Redis e o coletor OpenTelemetry.
- Traefik Labels configuram o roteamento: ele reconhece o container, escuta requisições para
localhost
, e encaminha para a porta interna3000
.
🌐 traefik
: proxy reverso e balanceador de carga
traefik: image: traefik:v3.0 container_name: traefik command: - '--api.dashboard=true' - '--api.insecure=true' - '--providers.docker=true' - '--providers.docker.exposedbydefault=false' - '--entrypoints.web.address=:80' - '--log.level=DEBUG' - '--accesslog=true' - '--accesslog.fields.defaultmode=keep' ports: - '80:80' - '8080:8080' volumes: - /var/run/docker.sock:/var/run/docker.sock:ro networks: - fyoussef
O que está sendo feito aqui:
- Traefik atua como proxy reverso e balanceador de carga, distribuindo requisições entre
app1
eapp2
. - Painel de controle acessível em
http://localhost:8080
, mostra os serviços ativos e como o tráfego está sendo roteado. - Configuração dinâmica via Docker: o Traefik lê as labels definidas em cada container e se autoconfigura.
- Logs ativados: tanto logs internos (
log.level=DEBUG
) quanto de acesso HTTP (accesslog=true
) para facilitar a depuração.
🧪 Resultado prático
Esse setup cria uma simulação realista de ambiente de produção com múltiplas instâncias da API, proxy reverso, limites de recurso e observabilidade ativa — tudo pronto para testar localmente, sem precisar de Kubernetes.
👉 Testar esse cenário localmente permite entender como a aplicação se comporta em ambientes escaláveis, e também facilita a identificação de gargalos de performance.
🧩 O que você pode aprender com esse projeto?
Mesmo que você esteja começando, esse projeto pode te ensinar bastante:
- Como usar Docker de forma eficiente
- Como fazer uma API que já vem pronta para escalar
- Como aplicar observabilidade desde o início
- Como montar um ambiente local que imita produção
💡 Quer testar?
Clone o repositório e suba o ambiente com um único comando:
git clone https://github.com/fyoussef/heroes-api.git cd heroes-api docker compose up --build # Teste de carga (requer Node): npx autocannon -c 100 -d 20 http://localhost/api/heroes
Acesse:
- API: http://localhost
- Traefik Dashboard: http://localhost:8080
- Jaeger (para ver os rastreamentos): http://localhost:16686
Top comments (0)