O Problema
Em projetos com múltiplas aplicações e bibliotecas compartilhadas, é comum enfrentar:
- Duplicação de código
- Inconsistência entre aplicações
- Dificuldade de versionamento
- Setup complexo para novos projetos
Esse cenário se torna ainda mais crítico quando começamos a trabalhar com design systems, bibliotecas internas e múltiplos produtos conectados.
Foi nesse contexto que comecei a adotar monorepo com pnpm.
Quando faz sentido usar monorepo
Monorepo não é solução universal. Ele resolve problemas específicos.
Use monorepo quando:
- Você tem múltiplas aplicações que compartilham código
- Existe um design system ou biblioteca interna
- Times trabalham em produtos relacionados
- Há necessidade de padronização entre projetos
Evite monorepo quando:
- O projeto é pequeno ou isolado
- Não há compartilhamento de código
- Times são totalmente independentes
- A complexidade não se justifica
Por que escolhi pnpm
Entre npm, yarn e pnpm, a escolha foi baseada em eficiência e previsibilidade.
Principais motivos:
- Workspace nativo simples
- Melhor performance na instalação
- Uso otimizado de disco (store compartilhado)
- Lockfile consistente
- Isolamento de dependências mais seguro
O que mudou na prática
Ao adotar monorepo com pnpm:
- Reduzi duplicação de código entre projetos
- Centralizei lógica compartilhada (UI, hooks, utils)
- Padronizei estrutura entre aplicações
- Melhorei onboarding de novos projetos
- Aumentei consistência entre produtos
Configurando um Monorepo com pnpm
Estrutura Básica
Vamos começar criando a estrutura básica de um monorepo:
monorepo/ ├── packages/ │ ├── utils/ │ ├── ui-components/ │ └── config/ ├── apps/ │ ├── web-app/ │ └── admin-panel/ ├── pnpm-workspace.yaml └── package.json
1. Configurando pnpm-workspace.yaml
O arquivo `pnpm-workspace.yaml` define quais diretórios são parte do workspace:
packages: - 'apps/*' - 'packages/*'
Isso informa ao pnpm que todos os diretórios dentro de `apps/` e `packages/` são pacotes do workspace.
2. package.json Raiz
O `package.json` na raiz gerencia scripts e dependências compartilhadas:
{
"name": "monorepo",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "pnpm --filter \"./apps/*\" dev",
"build": "pnpm --filter \"./packages/*\" build",
"test": "pnpm --filter \"./packages/*\" test",
"lint": "pnpm --filter \"./packages/*\" lint"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^20.0.0"
}
}3. Criando um Pacote
Vamos criar um pacote de utilitários como exemplo:
{
"name": "@monorepo/utils",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"dependencies": {},
"devDependencies": {
"typescript": "workspace:*"
}
}Pontos importantes:
- `workspace:*` referencia a versão do pacote no workspace
- Nome do pacote com escopo (`@monorepo/utils`)
- Scripts de build e desenvolvimento
4. Usando Pacotes Internos
Para usar um pacote interno em outro, você referencia pelo nome:
{
"name": "web-app",
"version": "1.0.0",
"dependencies": {
"@monorepo/utils": "workspace:*",
"@monorepo/ui-components": "workspace:*"
}
}O `workspace:*` indica que o pacote está no mesmo workspace.
Configurações Avançadas
Filtros e Scripts
pnpm permite executar comandos em pacotes específicos usando filtros:
{
"scripts": {
"dev:web": "pnpm --filter web-app dev",
"dev:admin": "pnpm --filter admin-panel dev",
"build:packages": "pnpm --filter \"./packages/*\" build",
"test:utils": "pnpm --filter @monorepo/utils test"
}
}Dependências Compartilhadas
Para compartilhar dependências entre todos os pacotes, você pode usar `.npmrc`:
# .npmrc na raiz shamefully-hoist=true
Ou declarar no `package.json` raiz e usar `pnpm.hoistPattern`:
{
"pnpm": {
"hoistPattern": [
"*react*",
"*typescript*"
]
}Versionamento com Changesets
Para gerenciar versionamento de múltiplos pacotes, use Changesets:
pnpm add -D -w @changesets/cli
{
"scripts": {
"changeset": "changeset",
"version": "changeset version",
"release": "pnpm build && changeset publish"
}
}Estrutura de um Pacote Completo
Vamos ver um exemplo completo de um pacote:
packages/utils/ ├── src/ │ ├── index.ts │ ├── formatCurrency.ts │ └── dateHelpers.ts ├── dist/ ├── package.json ├── tsconfig.json └── README.md
src/index.ts
export * from './formatCurrency'; export * from './dateHelpers';
tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}tsconfig.base.json (raiz)
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler"
}
}Scripts Úteis para Monorepo
Build Incremental
{
"scripts": {
"build": "pnpm --filter \"./packages/*\" --filter \"./apps/*\" build",
"build:changed": "pnpm --filter \"...[origin/main]\" build"
}
}Testes
{
"scripts": {
"test": "pnpm --filter \"./packages/*\" test",
"test:changed": "pnpm --filter \"...[origin/main]\" test",
"test:watch": "pnpm --filter \"./packages/*\" test --watch"
}
}Linting
{
"scripts": {
"lint": "pnpm --filter \"./packages/*\" --filter \"./apps/*\" lint",
"lint:fix": "pnpm --filter \"./packages/*\" --filter \"./apps/*\" lint --fix"
}
}Integração com Turborepo
Para builds ainda mais rápidos, combine pnpm com Turborepo:
pnpm add -D -w turbo
turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
}
}
}Scripts com Turborepo
{
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"dev": "turbo run dev"
}
}Boas Práticas
1. Nomenclatura Consistente
Use um escopo consistente para todos os pacotes:
{
"name": "@empresa/utils",
"name": "@empresa/ui-components",
"name": "@empresa/config"
}2. Dependências de Desenvolvimento
Mantenha dependências de desenvolvimento no nível raiz quando possível:
{
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^20.0.0",
"eslint": "^8.0.0"
}
}3. Workspace Protocol
Sempre use `workspace:*` para dependências internas:
{
"dependencies": {
"@monorepo/utils": "workspace:*"
}
}4. Estrutura de Pastas
Mantenha uma estrutura clara:
monorepo/ ├── packages/ # Pacotes compartilhados │ ├── utils/ │ ├── ui/ │ └── config/ ├── apps/ # Aplicações │ ├── web/ │ └── admin/ ├── tools/ # Scripts e ferramentas └── docs/ # Documentação
5. CI/CD Otimizado
Configure CI para construir apenas o que mudou:
- name: Install pnpm uses: pnpm/action-setup@v2 with: version: 8 - name: Get changed packages id: changed run: | echo "packages=$(pnpm --filter='...[origin/main]' list --depth=-1 --json | jq -r '.[].name')" >> $GITHUB_OUTPUT - name: Build changed packages run: pnpm --filter="...[origin/main]" build
Resolvendo Problemas Comuns
Dependências Não Encontradas
Se um pacote não encontra outro do workspace:
# Reinstalar dependências pnpm install
Builds Fracassando
Verifique a ordem de build. Use `dependsOn` no Turborepo ou scripts sequenciais:
{
"scripts": {
"build": "pnpm --filter \"./packages/*\" build && pnpm --filter \"./apps/*\" build"
}
}Cache Issues
Limpe o cache do pnpm:
pnpm store prune
Exemplo Prático Completo
Vamos criar um exemplo completo de configuração:
pnpm-workspace.yaml
packages: - 'apps/*' - 'packages/*'
package.json (raiz)
{
"name": "monorepo-example",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "pnpm --filter \"./apps/*\" dev",
"build": "pnpm --filter \"./packages/*\" build && pnpm --filter \"./apps/*\" build",
"test": "pnpm --filter \"./packages/*\" test",
"lint": "pnpm --filter \"./packages/*\" --filter \"./apps/*\" lint",
"clean": "pnpm --filter \"./packages/*\" --filter \"./apps/*\" clean"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^20.0.0"
}
}.npmrc
shamefully-hoist=true strict-peer-dependencies=false
Como isso foi útil no dia a dia
Implementação em projetos reais resultou em:
- Redução de 70% no tempo de instalação — Economizamos tempo valioso em cada setup de projeto
- Economia de 60% em espaço em disco — Importante quando trabalhamos com múltiplos projetos
- Aceleração de 3x em builds incrementais — Desenvolvimento mais rápido e feedback imediato
- Simplificação de 80% no compartilhamento de código — Reaproveitamento de código entre projetos ficou trivial
- Redução de 50% em problemas de versionamento — Menos conflitos e dependências desatualizadas
Conclusão
Monorepos com pnpm oferecem uma solução eficiente para gerenciar múltiplos pacotes. Workspaces nativos, performance superior e eficiência de espaço tornam o pnpm ideal para times que precisam de organização e produtividade.
A chave é começar simples, estabelecer padrões claros e evoluir conforme a necessidade. Com as ferramentas certas, um monorepo pode transformar a forma como seu time trabalha, acelerando desenvolvimento e melhorando a qualidade do código.
Este artigo reflete aprendizados práticos de configurar e manter monorepos com pnpm em projetos reais. Estratégias validadas em produção e aplicadas no dia a dia.