Voltar para o Blog

Monorepos em JavaScript: Guia Completo com Turborepo e Nx em 2025

Olá HaWkers, monorepos se tornaram a escolha padrão para times que gerenciam múltiplos pacotes ou aplicações relacionadas. Empresas como Google, Meta, Microsoft e Vercel usam monorepos em escala massiva.

Você já teve que manter múltiplos repositórios sincronizados, com versões diferentes de dependências e processos de release manuais? Monorepos resolvem esses problemas de forma elegante.

O Que é um Monorepo e Por Que Usar

Um monorepo é um único repositório que contém múltiplos projetos distintos, mas relacionados. Não é o mesmo que um "monolito" - cada projeto pode ser independente.

Vantagens dos Monorepos

Compartilhamento de código simplificado:

  • Pacotes internos são importados diretamente
  • Mudanças propagam instantaneamente
  • Não precisa publicar no npm para usar internamente

Atomic commits:

  • Mudanças que afetam múltiplos pacotes em um único commit
  • Refatorações coordenadas
  • Histórico de mudanças unificado

Ferramentas unificadas:

  • Uma configuração de ESLint, Prettier, TypeScript
  • CI/CD centralizado
  • Dependências deduplicadas

Desvantagens a considerar:

  • Curva de aprendizado inicial
  • Repositório pode ficar grande
  • Requer ferramentas especializadas para escalar

Estrutura Básica de um Monorepo

Estrutura de Diretórios

my-monorepo/
├── apps/                    # Aplicações
│   ├── web/                 # App Next.js
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── mobile/              # App React Native
│   │   ├── src/
│   │   └── package.json
│   └── api/                 # API Node.js
│       ├── src/
│       └── package.json

├── packages/                # Pacotes compartilhados
│   ├── ui/                  # Componentes de UI
│   │   ├── src/
│   │   └── package.json
│   ├── utils/               # Funções utilitárias
│   │   ├── src/
│   │   └── package.json
│   ├── config-eslint/       # Configuração ESLint
│   │   └── package.json
│   └── config-typescript/   # Configuração TypeScript
│       └── package.json

├── package.json             # Root package.json
├── pnpm-workspace.yaml      # Workspace config (pnpm)
├── turbo.json               # Turborepo config
└── tsconfig.json            # Base TypeScript config

Configuração do Workspace (pnpm)

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
// package.json (root)
{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "clean": "turbo run clean && rm -rf node_modules"
  },
  "devDependencies": {
    "turbo": "^2.0.0"
  },
  "packageManager": "pnpm@8.15.0"
}

Configurando Turborepo

Turborepo é a ferramenta mais popular para gerenciar monorepos JavaScript em 2025, adquirida pela Vercel.

Configuração Básica

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "clean": {
      "cache": false
    }
  }
}

Pipeline com Cache Remoto

// turbo.json - Configuração avançada
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": [
    ".env",
    "**/.env.*local"
  ],
  "globalEnv": [
    "NODE_ENV",
    "VERCEL_URL"
  ],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"],
      "env": ["DATABASE_URL", "API_KEY"]
    },
    "build:docker": {
      "dependsOn": ["build"],
      "outputs": ["Dockerfile.built"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"],
      "inputs": [
        "src/**/*.tsx",
        "src/**/*.ts",
        "**/*.test.ts",
        "**/*.test.tsx"
      ]
    },
    "test:e2e": {
      "dependsOn": ["build"],
      "outputs": ["playwright-report/**"],
      "inputs": ["e2e/**/*.ts"]
    },
    "lint": {
      "outputs": [],
      "inputs": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Pacotes Compartilhados

Pacote de UI Componentes

// packages/ui/package.json
{
  "name": "@myorg/ui",
  "version": "0.0.0",
  "private": true,
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.js"
    },
    "./button": {
      "types": "./dist/button.d.ts",
      "import": "./dist/button.mjs",
      "require": "./dist/button.js"
    }
  },
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts",
    "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
    "lint": "eslint src/",
    "clean": "rm -rf dist"
  },
  "devDependencies": {
    "@myorg/config-typescript": "workspace:*",
    "tsup": "^8.0.0",
    "typescript": "^5.3.0"
  },
  "peerDependencies": {
    "react": "^18.0.0"
  }
}
// packages/ui/src/index.ts
export { Button, type ButtonProps } from './button';
export { Input, type InputProps } from './input';
export { Card, type CardProps } from './card';
export { Modal, type ModalProps } from './modal';
// packages/ui/src/button.tsx
import { forwardRef, ButtonHTMLAttributes } from 'react';

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  isLoading?: boolean;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant = 'primary', size = 'md', isLoading, children, className, disabled, ...props }, ref) => {
    const baseStyles = 'inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';

    const variants = {
      primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
      secondary: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500',
      outline: 'border-2 border-gray-300 bg-transparent hover:bg-gray-100 focus:ring-gray-500',
      ghost: 'bg-transparent hover:bg-gray-100 focus:ring-gray-500',
    };

    const sizes = {
      sm: 'px-3 py-1.5 text-sm rounded',
      md: 'px-4 py-2 text-base rounded-md',
      lg: 'px-6 py-3 text-lg rounded-lg',
    };

    return (
      <button
        ref={ref}
        className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className || ''}`}
        disabled={disabled || isLoading}
        {...props}
      >
        {isLoading ? (
          <>
            <svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
              <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
              <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
            </svg>
            Carregando...
          </>
        ) : (
          children
        )}
      </button>
    );
  }
);

Button.displayName = 'Button';

Pacote de Utilitários

// packages/utils/src/index.ts
export * from './string';
export * from './date';
export * from './validation';
export * from './formatting';
// packages/utils/src/validation.ts
export const validators = {
  email: (value: string): boolean => {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(value);
  },

  cpf: (value: string): boolean => {
    const cpf = value.replace(/\D/g, '');
    if (cpf.length !== 11 || /^(\d)\1+$/.test(cpf)) return false;

    let sum = 0;
    for (let i = 0; i < 9; i++) {
      sum += parseInt(cpf.charAt(i)) * (10 - i);
    }
    let remainder = (sum * 10) % 11;
    if (remainder === 10 || remainder === 11) remainder = 0;
    if (remainder !== parseInt(cpf.charAt(9))) return false;

    sum = 0;
    for (let i = 0; i < 10; i++) {
      sum += parseInt(cpf.charAt(i)) * (11 - i);
    }
    remainder = (sum * 10) % 11;
    if (remainder === 10 || remainder === 11) remainder = 0;

    return remainder === parseInt(cpf.charAt(10));
  },

  phone: (value: string): boolean => {
    const phone = value.replace(/\D/g, '');
    return phone.length >= 10 && phone.length <= 11;
  },

  url: (value: string): boolean => {
    try {
      new URL(value);
      return true;
    } catch {
      return false;
    }
  },
};

export function createValidator<T extends Record<string, unknown>>(
  rules: Record<keyof T, (value: unknown) => string | null>
) {
  return (data: T): Record<keyof T, string | null> => {
    const errors = {} as Record<keyof T, string | null>;

    for (const [field, validator] of Object.entries(rules)) {
      errors[field as keyof T] = validator(data[field as keyof T]);
    }

    return errors;
  };
}

Usando Nx como Alternativa

Nx é mais opinativo que Turborepo e oferece mais ferramentas built-in.

Setup do Nx

npx create-nx-workspace@latest my-workspace --preset=ts
// nx.json
{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production"],
      "cache": true
    },
    "lint": {
      "inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
      "cache": true
    }
  },
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": [
      "default",
      "!{projectRoot}/**/*.spec.ts",
      "!{projectRoot}/**/*.test.ts"
    ],
    "sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"]
  },
  "plugins": [
    "@nx/vite/plugin",
    "@nx/eslint/plugin"
  ]
}

Geradores do Nx

# Criar nova biblioteca
npx nx generate @nx/js:library my-lib --directory=packages/my-lib

# Criar novo app React
npx nx generate @nx/react:application my-app --directory=apps/my-app

# Visualizar dependências
npx nx graph

Configurações Compartilhadas

ESLint Compartilhado

// packages/config-eslint/index.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'prettier',
  ],
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint', 'react', 'react-hooks'],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
    },
  },
  settings: {
    react: {
      version: 'detect',
    },
  },
  rules: {
    'react/react-in-jsx-scope': 'off',
    'react/prop-types': 'off',
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-explicit-any': 'warn',
  },
  ignorePatterns: ['dist', 'node_modules', '.turbo'],
};
// apps/web/.eslintrc.js
module.exports = {
  root: true,
  extends: ['@myorg/config-eslint'],
  parserOptions: {
    project: './tsconfig.json',
    tsconfigRootDir: __dirname,
  },
};

TypeScript Compartilhado

// packages/config-typescript/base.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "declaration": true,
    "declarationMap": true,
    "inlineSources": false,
    "preserveWatchOutput": true
  },
  "exclude": ["node_modules"]
}
// packages/config-typescript/react.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "extends": "./base.json",
  "compilerOptions": {
    "lib": ["DOM", "DOM.Iterable", "ES2022"],
    "jsx": "react-jsx",
    "module": "ESNext",
    "target": "ES2022"
  }
}

CI/CD para Monorepos

GitHub Actions Otimizado

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build affected packages
        run: pnpm turbo run build --filter=...[HEAD^1]

      - name: Test affected packages
        run: pnpm turbo run test --filter=...[HEAD^1]

      - name: Lint affected packages
        run: pnpm turbo run lint --filter=...[HEAD^1]

  deploy:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile

      - name: Build all
        run: pnpm turbo run build

      - name: Deploy web
        run: pnpm --filter=@myorg/web deploy
        env:
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}

Conclusão

Monorepos oferecem benefícios significativos para times que gerenciam múltiplos pacotes relacionados. As ferramentas modernas como Turborepo e Nx tornaram a gestão de monorepos acessível mesmo para times menores.

Quando usar monorepo:

  • Múltiplos pacotes com código compartilhado
  • Times trabalhando em features cross-cutting
  • Necessidade de releases coordenadas

Quando evitar:

  • Projetos completamente independentes
  • Times com baixa comunicação
  • Repositório único muito pequeno

Se você quer aprofundar seus conhecimentos em arquitetura JavaScript, recomendo que dê uma olhada em outro artigo: Vite vs Webpack em 2025 onde você vai descobrir como escolher a ferramenta de build ideal para seus pacotes.

Bora pra cima! 🦅

🎯 Junte-se aos Desenvolvedores que Estão Evoluindo

Milhares de desenvolvedores já usam nosso material para acelerar seus estudos e conquistar melhores posições no mercado.

Por que investir em conhecimento estruturado?

Aprender de forma organizada e com exemplos práticos faz toda diferença na sua jornada como desenvolvedor.

Comece agora:

  • 1x de R$9,90 no cartão
  • ou R$9,90 à vista

🚀 Acessar Guia Completo

"Material excelente para quem quer se aprofundar!" - João, Desenvolvedor

Comentários (0)

Esse artigo ainda não possui comentários 😢. Seja o primeiro! 🚀🦅

Adicionar comentário