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 configConfiguraçã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
"Material excelente para quem quer se aprofundar!" - João, Desenvolvedor

