Monorepo com Nx: organização, cache e geração de código automatizada

1. Introdução ao Monorepo e ao Nx

Um monorepo é uma estratégia de gerenciamento de código onde múltiplos projetos — aplicações, bibliotecas, serviços — residem em um único repositório. Diferente do modelo multirepo (um repositório por projeto), o monorepo oferece vantagens significativas: visibilidade unificada do código, compartilhamento facilitado de bibliotecas, refatoração cross-projeto sem fricção, e padronização de ferramentas e configurações.

O Nx é uma ferramenta de build inteligente e orquestração de monorepos desenvolvida pela Nrwl. Ele vai além de simplesmente organizar pastas: oferece cache distribuído, execução paralela de tarefas, geração automatizada de código, e análise de dependências entre projetos. O Nx é ideal para equipes que trabalham com múltiplos frameworks (React, Angular, Node.js, Next.js, NestJS) em um mesmo repositório, especialmente em projetos de médio a grande porte onde o tempo de build e a consistência do código são críticos.

2. Configuração Inicial de um Monorepo com Nx

Para criar um workspace Nx, utilize o comando:

npx create-nx-workspace@latest meu-monorepo

Durante a configuração interativa, você pode escolher entre presets como apps (padrão), packages (para monorepos baseados em pacotes), ou ts (TypeScript puro). A estrutura gerada será similar a:

meu-monorepo/
├── apps/
│   ├── meu-app/
│   └── meu-api/
├── libs/
│   ├── shared/
│   ├── ui/
│   └── data-access/
├── tools/
│   ├── generators/
│   └── scripts/
├── nx.json
├── package.json
└── workspace.json (ou project.json em cada projeto)

Para adicionar suporte a múltiplos frameworks, use plugins do Nx:

nx add @nx/react
nx add @nx/node
nx add @nx/angular

Cada plugin adiciona geradores, executores e configurações específicas para o framework.

3. Organização de Código com Projetos e Bibliotecas

No Nx, um projeto é uma aplicação que pode ser executada (ex.: um frontend React ou uma API Node). Uma biblioteca (lib) é um conjunto de código reutilizável que não é executável diretamente, mas que pode ser importado por projetos ou outras libs.

Boas práticas de modularização incluem:

  • Separação por domínio: libs como @meu-monorepo/orders, @meu-monorepo/products
  • Separação por funcionalidade: libs como @meu-monorepo/ui, @meu-monorepo/data-access
  • Uso de tags para restringir dependências:
// nx.json
{
  "projects": {
    "meu-app": { "tags": ["scope:app", "type:app"] },
    "shared-ui": { "tags": ["scope:shared", "type:ui"] }
  }
}

Com restrições de escopo (enforce-module-boundaries), você impede que libs de baixo nível dependam de libs de alto nível, mantendo a arquitetura limpa.

4. Cache Inteligente e Otimização de Builds

O Nx implementa cache baseado em hash do conteúdo dos arquivos, configurações e dependências. Quando uma tarefa é executada (build, teste, lint), o Nx calcula um hash único. Se o hash for igual ao de uma execução anterior, o resultado é restaurado do cache, evitando o rebuild.

Para configurar cache local:

// nx.json
{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "test", "lint"],
        "cacheDirectory": ".cache/nx"
      }
    }
  }
}

Para cache remoto (Nx Cloud ou self-hosted):

nx connect-to-nx-cloud

Estratégias de hashing incluem o conteúdo dos arquivos, variáveis de ambiente, e versões de dependências. A invalidação ocorre automaticamente quando qualquer entrada muda, garantindo máxima eficiência.

5. Geração de Código Automatizada com Generators

O Nx possui generators nativos para criar projetos, libs, componentes, serviços e muito mais. Exemplos:

# Criar uma nova biblioteca
nx g @nx/react:lib shared-ui

# Criar um componente React
nx g @nx/react:component Button --project=shared-ui

# Criar um serviço Node
nx g @nx/node:service user-service --project=meu-api

Para criar generators personalizados, utilize:

nx g @nx/workspace:generator meu-generator

Isso cria uma estrutura em tools/generators/meu-generator/. Exemplo de generator que cria um serviço com padrões predefinidos:

// tools/generators/meu-generator/index.ts
import { Tree, formatFiles, generateFiles, joinPathFragments } from '@nx/devkit';

export default async function (tree: Tree, schema: any) {
  const substitutions = { name: schema.name, normalizedName: schema.name.toLowerCase() };
  generateFiles(tree, joinPathFragments(__dirname, 'files'), schema.path, substitutions);
  await formatFiles(tree);
}

Com isso, sua equipe pode gerar código consistente com um simples comando.

6. Execução Eficiente de Tarefas e Pipeline de CI/CD

Comandos essenciais do Nx:

# Executar tarefas em um projeto específico
nx build meu-app
nx test shared-ui
nx lint meu-api

# Executar tarefas em paralelo
nx run-many --target=build --projects=meu-app,meu-api

# Executar apenas em projetos afetados por mudanças
nx affected:test
nx affected:build --base=main --head=HEAD

O comando nx affected é especialmente útil em CI/CD. Exemplo de pipeline com GitHub Actions:

# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v3
        with:
          node-version: 18
      - run: npm ci
      - run: npx nx affected:build --base=origin/main --parallel=3
      - run: npx nx affected:test --base=origin/main --parallel=3

Com cache remoto habilitado, builds subsequentes serão drasticamente mais rápidos.

7. Migração e Integração com Projetos Existentes

Migrar um monorepo manual ou multirepo para Nx pode ser feito gradualmente:

  1. Crie um novo workspace Nx
  2. Mova os projetos existentes para apps/ ou libs/
  3. Configure project.json para cada projeto com os targets de build, teste, etc.
  4. Utilize nx migrate para atualizar configurações

O Nx suporta ferramentas externas como Webpack, Vite, Jest, Cypress e Storybook através de plugins. Para integrar, basta adicionar o plugin correspondente:

nx add @nx/vite
nx add @nx/cypress
nx add @nx/storybook

Para evitar conflitos de dependências, mantenha versões consistentes no package.json raiz e utilize resolutions ou overrides quando necessário.

8. Boas Práticas e Armadilhas Comuns

Boas práticas:

  • Utilize nx format:write para padronizar formatação
  • Configure changelogs automáticos com nx changelog
  • Monitore o tamanho do cache com nx report
  • Defina tags de escopo desde o início do projeto

Armadilhas comuns:

  • Dependências circulares: evite que lib A importe lib B que importa lib A. Use nx graph para visualizar dependências.
  • Configurações incorretas de tags: sem restrições de escopo, a arquitetura pode se degradar rapidamente.
  • Falhas de cache: se o cache não está sendo usado, verifique se as operações estão marcadas como cacheableOperations e se o hash está sendo calculado corretamente.

O Nx é uma ferramenta poderosa que, quando bem configurada, transforma a produtividade de equipes que gerenciam múltiplos projetos em um único repositório.

Referências