Os npm Workspaces no Node.js permitem gerenciar vários pacotes dentro de um único repositório. Uma aplicação pode manter API, workers, biblioteca compartilhada, cliente e ferramentas internas sob o mesmo lockfile, com instalação centralizada e links automáticos entre pacotes locais.
O recurso reduz a necessidade de npm link, facilita executar scripts em um workspace específico ou em todos e mantém versões de dependências coordenadas. Porém, um monorepo sem limites claros pode criar acoplamento, builds lentos e publicação acidental de pacotes privados.
Neste guia, você aprenderá a criar um monorepo com npm Workspaces, adicionar dependências, consumir pacotes locais, executar scripts, configurar TypeScript, CI, publicação e boas práticas de arquitetura.
O que são npm Workspaces?
A documentação de npm Workspaces define workspaces como recursos para gerenciar vários pacotes locais a partir de um pacote raiz. Durante npm install, pacotes declarados são vinculados automaticamente ao node_modules da raiz.
Uma estrutura comum:
meu-projeto/
├── package.json
├── package-lock.json
├── apps/
│ ├── api/
│ │ └── package.json
│ └── worker/
│ └── package.json
└── packages/
├── domain/
│ └── package.json
└── config/
└── package.jsonPackage raiz
{
"name": "plataforma",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"test": "npm run test --workspaces --if-present",
"build": "npm run build --workspaces --if-present",
"lint": "npm run lint --workspaces --if-present"
}
}private: true impede publicação acidental do pacote raiz.
Criando um workspace
npm init -w ./packages/domainO npm cria a pasta, o package.json e atualiza a configuração da raiz quando necessário.
Package de biblioteca
{
"name": "@empresa/domain",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./dist/index.js"
},
"types": "./dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsc -p tsconfig.build.json",
"test": "node --test"
}
}O nome do pacote é usado na resolução local, como seria após publicação.
Adicionando dependência a um workspace
npm install zod -w @empresa/domainA dependência é gravada no package.json do workspace, enquanto a instalação física pode ser otimizada na raiz.
Dependência entre workspaces
npm install @empresa/domain -w @empresa/apiO npm detecta o pacote local pelo nome e cria o link apropriado. A versão declarada precisa permanecer coerente com o pacote.
Consumindo pacote local
import { Order } from '@empresa/domain';Evite imports por caminhos relativos que atravessem workspaces:
// Evite
import { Order } from '../../../packages/domain/src/order.js';O import pelo nome testa o mesmo contrato que consumidores externos usariam.
Um único lockfile
A raiz mantém package-lock.json. Isso facilita instalação reproduzível:
npm ciNão mantenha lockfiles separados dentro dos workspaces, salvo uma necessidade específica e compreendida.
Executando script em um workspace
npm run test --workspace=@empresa/domainForma curta:
npm run test -w @empresa/domainExecutando em todos
npm run test --workspacesPara ignorar pacotes sem o script:
npm run test --workspaces --if-presentA documentação informa que a ordem segue a lista de workspaces do package.json. Não dependa dessa ordem para builds complexos; use uma ferramenta que compreenda o grafo ou scripts explícitos.
Scripts na raiz
{
"scripts": {
"dev:api": "npm run dev -w @empresa/api",
"dev:worker": "npm run dev -w @empresa/worker",
"test": "npm run test --workspaces --if-present",
"format": "prettier . --write"
}
}Veja Prettier no Node.js para formatação centralizada.
TypeScript
Uma configuração base pode ficar na raiz:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"declaration": true,
"sourceMap": true
}
}Cada workspace estende:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}Project references
TypeScript Project References ajudam em builds incrementais:
{
"files": [],
"references": [
{ "path": "packages/domain" },
{ "path": "apps/api" }
]
}Configure composite: true nos projetos referenciados e valide o grafo.
Exports
Não exponha toda a pasta src. Defina uma API pública:
{
"exports": {
".": "./dist/index.js",
"./errors": "./dist/errors.js"
}
}O próximo artigo aprofunda exports e imports.
ESLint centralizado
Uma configuração flat na raiz pode usar padrões por workspace:
{
files: ['apps/**/*.ts', 'packages/**/*.ts'],
rules: {
'no-console': 'error'
}
}Consulte ESLint Flat Config no Node.js.
Dependências compartilhadas
Mesmo que o npm faça hoisting, cada workspace deve declarar diretamente tudo que importa. Não dependa de uma biblioteca aparecer no node_modules porque outro pacote a utiliza.
Teste pacotes com instalação empacotada para descobrir dependências fantasmas.
Dependências de desenvolvimento
Ferramentas usadas por todos, como ESLint e Prettier, podem ficar na raiz. Ferramentas exclusivas de um pacote devem ser declaradas nele. Mantenha a regra consistente para reduzir confusão.
Versões internas
Pacotes publicados precisam de versões reais. Pacotes exclusivamente privados podem usar private: true, mas ainda é útil manter versões coerentes para cache e diagnóstico.
Publicação
npm publish -w @empresa/domainAntes, simule:
npm pack -w @empresa/domain --dry-runConfirme arquivos, exports, README, licença e versão.
Pacotes privados
{
"name": "@empresa/api",
"private": true
}Aplicações que não devem ir ao registry precisam de private: true.
CI
- run: npm ci
- run: npm run lint
- run: npm run test
- run: npm run buildO artigo CI para Node.js com GitHub Actions mostra cache, services e segurança.
CI por caminhos
Monorepos grandes podem filtrar jobs por mudanças, mas considere dependências transitivas. Alterar domain deve testar API e worker que o consomem.
Docker
Copie primeiro manifests e lockfile para aproveitar cache:
COPY package.json package-lock.json ./
COPY apps/api/package.json apps/api/package.json
COPY packages/domain/package.json packages/domain/package.json
RUN npm ci
COPY . .
RUN npm run build -w @empresa/apiConsulte Docker Multi-stage para Node.js.
Evite acoplamento acidental
Um monorepo facilita imports, mas não elimina fronteiras. Defina:
- API pública por pacote;
- direção permitida de dependências;
- pacotes de domínio sem framework;
- infraestrutura isolada;
- regras que bloqueiam imports internos.
Pacote compartilhado gigante
Evite um pacote common que recebe qualquer função reutilizável. Crie bibliotecas coesas, como domain, observability e testing.
Testes
Execute testes unitários por workspace e integração na aplicação consumidora. Um pacote pode passar isoladamente e falhar devido a exports, build ou resolução.
Versionamento independente ou único
Monorepos podem usar:
- fixed: todos os pacotes compartilham versão;
- independent: cada pacote evolui separadamente.
A escolha depende de publicação e acoplamento. Changesets ajuda a automatizar ambos.
Comandos úteis
npm ls --workspaces
npm outdated --workspaces
npm update --workspaces
npm run build --workspaces --if-present
npm exec --workspace=@empresa/api -- node -vErros comuns
- Import relativo entre pacotes: quebra encapsulamento.
- Dependência fantasma: não está declarada.
- Raiz publicável: risco de publicação acidental.
- Build sem grafo: consumidor compila antes da biblioteca.
- Pacote common gigante: acoplamento cresce.
- CI testa só pacote alterado: consumidores quebram.
- Exports ausentes: consumidores acessam internals.
- Lockfiles múltiplos: instalações divergem.
Estrutura recomendada
plataforma/
├── apps/
│ ├── api/
│ └── worker/
├── packages/
│ ├── domain/
│ ├── observability/
│ └── testing/
├── eslint.config.js
├── prettier.config.mjs
├── tsconfig.base.json
├── package-lock.json
└── package.jsonConclusão
Os npm Workspaces no Node.js simplificam monorepos ao centralizar instalação e criar links entre pacotes locais. A mesma resolução usada durante desenvolvimento se aproxima da forma como pacotes publicados são consumidos.
Use nomes de pacote, exports explícitos, dependências declaradas e private: true onde necessário. Com scripts, TypeScript, CI e limites arquiteturais, workspaces oferecem compartilhamento sem transformar o repositório em um bloco fortemente acoplado.




