Como estruturar um monorepo backend com múltiplos serviços em Node.js
1. Fundamentos e escolha da ferramenta de monorepo
Monorepo é uma abordagem onde múltiplos projetos compartilham o mesmo repositório. Para backends com múltiplos serviços em Node.js, essa estratégia oferece vantagens significativas: compartilhamento de tipos, utilitários e configurações entre serviços, reduzindo duplicação e garantindo consistência.
As principais ferramentas disponíveis são:
- Nx: Suporte a cache distribuído, geração de código e dependência entre projetos. Ideal para equipes grandes.
- Turborepo: Foco em cache local e paralelismo. Excelente para times médios.
- Lerna: Maduro, mas com manutenção reduzida. Bom para projetos legados.
- Yarn/NPM Workspaces puros: Simples e sem overhead. Ideal para times pequenos.
Para um projeto real, recomendo pnpm + Turborepo ou pnpm + Nx, combinando eficiência de instalação com cache inteligente.
# Exemplo de package.json root com workspaces
{
"name": "my-backend-monorepo",
"private": true,
"workspaces": [
"services/*",
"packages/*"
],
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint"
},
"devDependencies": {
"turbo": "^1.10.0",
"typescript": "^5.0.0"
}
}
2. Estrutura de diretórios e naming conventions
Organizar o monorepo por domínio facilita a escalabilidade. A estrutura recomendada:
my-backend-monorepo/
├── services/
│ ├── auth/
│ ├── payments/
│ └── notifications/
├── packages/
│ ├── shared-types/
│ ├── config/
│ ├── event-schemas/
│ └── utils/
├── tools/
│ └── scripts/
├── turbo.json
├── pnpm-workspace.yaml
└── tsconfig.base.json
Nomenclatura de pacotes internos segue o padrão @org/nome-do-pacote:
// packages/shared-types/package.json
{
"name": "@myorg/shared-types",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts"
}
Para importações entre pacotes, use tsconfig.json com paths e references:
// tsconfig.base.json
{
"compilerOptions": {
"paths": {
"@myorg/shared-types": ["packages/shared-types/src"],
"@myorg/config": ["packages/config/src"]
}
}
}
3. Configuração de ferramentas e dependências compartilhadas
Centralize dependências comuns no root package.json:
// package.json (root)
{
"devDependencies": {
"typescript": "^5.3.0",
"eslint": "^8.50.0",
"prettier": "^3.0.0",
"jest": "^29.0.0",
"@types/node": "^20.0.0"
}
}
Use pnpm para instalação eficiente. Configure o pnpm-workspace.yaml:
# pnpm-workspace.yaml
packages:
- 'services/*'
- 'packages/*'
Scripts raiz para build, lint e test em paralelo:
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
}
}
}
4. Compartilhamento de tipos e contratos entre serviços
Crie um pacote @myorg/shared-types com interfaces de DTOs e schemas de validação:
// packages/shared-types/src/user.ts
import { z } from 'zod';
export const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1),
createdAt: z.date()
});
export type User = z.infer<typeof UserSchema>;
export interface CreateUserDTO {
email: string;
name: string;
password: string;
}
export interface UserEvent {
type: 'USER_CREATED' | 'USER_UPDATED' | 'USER_DELETED';
payload: User;
timestamp: number;
}
Use tsc --build com project references para compilação incremental:
// packages/shared-types/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"composite": true,
"declaration": true
},
"include": ["src"]
}
Versionamento semântico interno: siga MAJOR.MINOR.PATCH para pacotes compartilhados. Mudanças que quebram contratos exigem major version.
5. Comunicação entre serviços (API Gateway e eventos)
Implemente um gateway centralizado com Fastify:
// services/gateway/src/index.ts
import Fastify from 'fastify';
import { authRoutes } from './routes/auth';
import { paymentsRoutes } from './routes/payments';
const app = Fastify({ logger: true });
app.register(authRoutes, { prefix: '/api/auth' });
app.register(paymentsRoutes, { prefix: '/api/payments' });
app.listen({ port: 3000 });
Para mensageria assíncrona, use RabbitMQ com contratos tipados:
// packages/event-schemas/src/events.ts
export interface PaymentProcessedEvent {
type: 'PAYMENT_PROCESSED';
payload: {
userId: string;
amount: number;
currency: string;
status: 'success' | 'failed';
};
}
// services/notifications/src/consumer.ts
import { PaymentProcessedEvent } from '@myorg/event-schemas';
async function handlePaymentProcessed(event: PaymentProcessedEvent) {
// Enviar notificação ao usuário
}
6. Gerenciamento de configurações e variáveis de ambiente
Crie um pacote @myorg/config que carrega e valida env vars:
// packages/config/src/index.ts
import dotenv from 'dotenv';
import { z } from 'zod';
dotenv.config();
const envSchema = z.object({
NODE_ENV: z.enum(['dev', 'staging', 'production']).default('dev'),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url().optional(),
RABBITMQ_URL: z.string().url().optional()
});
export const config = envSchema.parse(process.env);
export type Config = z.infer<typeof envSchema>;
Perfis de configuração por ambiente:
# .env.example
NODE_ENV=dev
PORT=3000
DATABASE_URL=postgresql://localhost:5432/mydb
REDIS_URL=redis://localhost:6379
RABBITMQ_URL=amqp://localhost:5672
Script de validação pré-build:
// tools/scripts/validate-env.sh
#!/bin/bash
for service in services/*/; do
if [ -f "$service/.env.example" ]; then
echo "Validando $service..."
node -e "
require('dotenv').config({ path: '$service/.env' });
const { config } = require('@myorg/config');
console.log('Config válida para', '$service');
"
fi
done
7. CI/CD, testes e deploy integrado
Pipeline único de CI com detecção de mudanças:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install
- run: pnpm build
- run: pnpm test -- --filter=[changed-packages]
Deploy independente por serviço com Docker multi-stage:
# services/auth/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY pnpm-lock.yaml ./
COPY package.json ./
RUN pnpm install
COPY . .
RUN pnpm build
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3001
CMD ["node", "dist/index.js"]
Scripts de release versionados:
// tools/scripts/release.sh
#!/bin/bash
VERSION=$1
SERVICE=$2
if [ -z "$VERSION" ] || [ -z "$SERVICE" ]; then
echo "Uso: ./release.sh <version> <service>"
exit 1
fi
cd services/$SERVICE
npm version $VERSION
git tag "$SERVICE-v$VERSION"
git push --tags
docker build -t "myorg/$SERVICE:$VERSION" .
docker push "myorg/$SERVICE:$VERSION"
Conclusão
Estruturar um monorepo backend com múltiplos serviços em Node.js exige planejamento cuidadoso, mas os benefícios são enormes: compartilhamento de código, consistência entre serviços, CI/CD eficiente e deploy independente. A combinação de pnpm + Turborepo + TypeScript oferece a base ideal para projetos escaláveis.
Lembre-se de versionar pacotes compartilhados com cuidado, validar variáveis de ambiente e manter contratos de eventos bem definidos. Com essa estrutura, sua equipe pode evoluir múltiplos serviços de forma coordenada e eficiente.
Referências
- pnpm Workspaces Documentation — Documentação oficial do pnpm sobre workspaces, incluindo configuração e boas práticas para monorepos.
- Turborepo Documentation — Guia completo do Turborepo, com exemplos de pipeline, cache e configuração para monorepos Node.js.
- TypeScript Project References — Documentação oficial sobre project references no TypeScript, essencial para compilação incremental em monorepos.
- Fastify Documentation — Documentação oficial do Fastify, framework utilizado para implementar o API Gateway no exemplo.
- Zod Documentation — Biblioteca de validação de schemas TypeScript, usada nos exemplos de validação de env vars e DTOs.
- RabbitMQ Official Tutorials — Tutoriais oficiais do RabbitMQ para implementação de mensageria assíncrona entre serviços.
- Docker Multi-stage Builds — Documentação oficial sobre builds multi-stage no Docker, utilizado para deploy independente de serviços.