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.