O recurso npm Workspaces permite administrar vários pacotes Node.js dentro de um único repositório, com instalação centralizada, links locais automáticos e comandos executados por pacote ou em todo o monorepo. Ele reduz a necessidade de usar npm link manualmente e ajuda equipes a manter bibliotecas, serviços e aplicações relacionadas no mesmo projeto.
Um workspace é um pacote com seu próprio package.json localizado abaixo de um pacote raiz. O arquivo raiz lista quais diretórios fazem parte do conjunto. Durante npm install, o npm reconhece esses pacotes e cria os links necessários em node_modules.
Estrutura básica de um monorepo
Uma estrutura simples pode separar aplicações e bibliotecas reutilizáveis:
meu-projeto/
├── package.json
├── package-lock.json
├── apps/
│ ├── api/
│ │ └── package.json
│ └── worker/
│ └── package.json
└── packages/
├── config/
│ └── package.json
└── logger/
└── package.jsonNo package.json raiz, declare padrões ou caminhos explícitos:
{
"name": "plataforma",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
]
}O campo private evita publicar acidentalmente o pacote raiz. Cada workspace continua podendo ser publicado separadamente, desde que sua configuração permita.
Criando workspaces com npm init
É possível criar a estrutura manualmente ou usar o comando:
npm init -w ./packages/logger
npm init -w ./apps/apiO npm cria os diretórios, gera o package.json e atualiza a lista de workspaces do projeto raiz quando necessário.
Defina nomes únicos nos pacotes internos. Pacotes privados podem usar um escopo da organização:
{
"name": "@empresa/logger",
"version": "1.0.0",
"type": "module",
"private": true
}Instalação centralizada
Execute npm install na raiz. O npm gera um único package-lock.json para todo o monorepo e organiza dependências compartilhadas. Quando dois workspaces precisam da mesma versão de uma biblioteca, ela geralmente pode ser instalada no nível superior. Dependências incompatíveis ainda podem aparecer em níveis específicos.
Esse comportamento reduz duplicação, mas não significa que qualquer pacote possa usar dependências não declaradas. Cada workspace deve listar explicitamente o que importa. Caso contrário, o código pode funcionar localmente por causa do hoisting e falhar quando o pacote for publicado ou instalado isoladamente.
Adicionando dependências a um workspace
Use -w ou --workspace para escolher o destino:
npm install fastify -w @empresa/api
npm install pino -w @empresa/logger
npm install -D typescript -w @empresa/apiO npm altera o package.json do workspace selecionado, não o arquivo raiz.
Também é possível adicionar um workspace como dependência de outro:
npm install @empresa/logger -w @empresa/apiComo o pacote existe localmente, o npm cria um link em vez de baixá-lo do registro. A dependência ainda recebe um intervalo de versão normal no package.json, o que é importante para publicação e versionamento.
Executando scripts em um pacote
Para executar um script em um workspace específico:
npm run test --workspace=@empresa/api
npm run build -w @empresa/loggerPara executar em todos:
npm run test --workspaces
npm run lint --workspaces --if-present--if-present ignora pacotes que não possuem o script. Sem essa opção, a ausência pode interromper o comando.
A ordem segue a sequência declarada no campo workspaces. Isso não substitui um grafo de dependências. Se o pacote B depende do pacote A, não confie apenas na ordem da lista; crie scripts explícitos ou use uma ferramenta de orquestração quando o projeto exigir execução topológica e cache.
Scripts úteis na raiz
Centralize tarefas comuns:
{
"scripts": {
"test": "npm run test --workspaces --if-present",
"lint": "npm run lint --workspaces --if-present",
"build": "npm run build --workspaces --if-present",
"dev:api": "npm run dev -w @empresa/api",
"dev:worker": "npm run dev -w @empresa/worker"
}
}Isso oferece uma interface única para desenvolvedores e para o CI.
Versionamento de pacotes
Existem duas estratégias principais. No versionamento fixo, todos os pacotes compartilham a mesma versão e são publicados juntos. No independente, cada pacote evolui separadamente.
O npm Workspaces fornece a estrutura, mas não administra changelogs e versões complexas sozinho. Para bibliotecas publicadas, combine-o com Changesets ou outra ferramenta de release. Em aplicações privadas, versões podem servir apenas para expressar compatibilidade interna.
Sempre atualize dependências internas quando houver mudança incompatível. Um pacote que depende de @empresa/logger deve declarar uma faixa coerente com a versão realmente testada.
package-lock.json e npm ci
Mantenha o lockfile raiz no controle de versão. Em CI, use:
npm ciO comando instala exatamente as versões registradas e falha se package.json e package-lock.json estiverem inconsistentes.
Evite executar instalações independentes dentro de cada workspace, pois isso pode gerar lockfiles concorrentes e ambientes diferentes. O fluxo padrão deve começar na raiz.
Imports entre pacotes
Importe pelo nome declarado no pacote, não por caminhos relativos que atravessam diretórios:
import { logger } from '@empresa/logger';Evite:
import { logger } from '../../../packages/logger/src/index.js';O primeiro formato respeita a API pública e se comporta como ocorrerá após a publicação. O segundo cria acoplamento à estrutura do repositório.
Defina corretamente exports e types no pacote interno para que Node.js, TypeScript e editores resolvam os arquivos certos.
TypeScript em workspaces
Uma configuração compartilhada pode ficar em packages/config ou na raiz. Cada pacote estende essa base:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}Não use aliases TypeScript como substitutos de dependências reais. Um alias pode funcionar durante a compilação, mas não existir na resolução do Node.js. Prefira nomes de pacotes e mapas de exports.
Testes e isolamento
Cada workspace deve ter testes próprios. Além disso, execute testes de integração envolvendo vários pacotes. Um bom pipeline inclui:
- instalação com
npm ci; - lint em todos os workspaces;
- build das bibliotecas antes das aplicações dependentes;
- testes unitários por pacote;
- testes de integração do conjunto;
npm packpara validar pacotes publicáveis.
Docker e workspaces
Ao criar uma imagem, copie primeiro os manifestos e o lockfile para aproveitar cache:
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
COPY apps/api/package.json apps/api/package.json
COPY packages/logger/package.json packages/logger/package.json
RUN npm ci
FROM deps AS build
COPY . .
RUN npm run build --workspaces --if-presentUma cópia incompleta dos manifestos pode fazer o npm ignorar workspaces ou invalidar o lockfile. Inclua todos os package.json necessários antes de instalar.
Boas práticas de segurança
Use npm audit, atualize dependências e não armazene tokens em arquivos versionados. Pacotes privados devem ter configuração de acesso explícita. Em CI, prefira OIDC ou tokens de escopo reduzido.
Revise scripts preinstall, postinstall e dependências transitivas. Um monorepo centraliza dependências, mas também amplia o impacto de uma alteração maliciosa ou quebrada.
Quando npm Workspaces é suficiente
Para monorepos pequenos e médios, o recurso nativo costuma bastar. Ele resolve links locais, instalação, lockfile e execução de comandos. Projetos grandes podem adicionar ferramentas para cache remoto, análise de dependências e execução incremental, sem abandonar o npm como gerenciador.
O ponto principal é manter limites claros entre pacotes. Combine workspaces com Exports e Imports no Node.js, pipelines de GitHub Actions no Node.js, imagens com GitHub Container Registry e segurança de dependências descrita em Segurança de Dependências no Node.js.
Consulte a documentação oficial de npm Workspaces e a referência oficial do npm run-script.



