Monorepos com pnpm oferecem uma solução eficiente para gerenciar múltiplos pacotes e projetos. Compartilho aprendizados práticos de como isso foi útil no dia a dia, acelerando desenvolvimento e melhorando a organização do código.

O Que é um Monorepo?

Um monorepo é uma estratégia de versionamento onde múltiplos projetos ou pacotes são armazenados em um único repositório Git. Em vez de ter repositórios separados para cada pacote, tudo fica organizado em uma estrutura hierárquica.

Vantagens principais:

  • Compartilhamento de código simplificado
  • Refatorações que afetam múltiplos pacotes em uma única PR
  • Versionamento coordenado
  • Builds incrementais e cache compartilhado
  • Visibilidade completa do código

Desafios:

  • Repositório maior
  • Necessidade de ferramentas adequadas
  • Gerenciamento de dependências mais complexo

Por Que pnpm para Monorepos?

pnpm é um gerenciador de pacotes que oferece várias vantagens para monorepos:

1. Eficiência de Espaço em Disco

pnpm usa um store global e links simbólicos, evitando duplicação de dependências. Em um monorepo com 10 pacotes que compartilham as mesmas dependências, você economiza espaço significativo.

# Comparação aproximada de espaço
npm/yarn: ~500MB por projeto = 5GB para 10 projetos
pnpm: ~500MB total (compartilhado)

2. Performance Superior

pnpm é mais rápido que npm e yarn, especialmente em monorepos grandes. O sistema de cache e links simbólicos reduz drasticamente o tempo de instalação.

3. Strict Dependency Resolution

pnpm garante que apenas dependências declaradas explicitamente podem ser acessadas, prevenindo problemas de "phantom dependencies" comuns em monorepos.

4. Workspaces Nativo

pnpm tem suporte nativo a workspaces, similar ao Yarn, mas com melhor performance e gerenciamento de dependências.

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.