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.