Os campos exports e imports do package.json permitem controlar com precisão quais arquivos de um pacote Node.js fazem parte da API pública e como caminhos internos são resolvidos. Eles substituem grande parte das soluções improvisadas que dependiam apenas de main, caminhos relativos longos ou acesso direto a arquivos internos dentro de node_modules.
Quando um pacote cresce, definir essa fronteira deixa de ser um detalhe. Sem uma API explícita, consumidores podem importar arquivos que nunca deveriam ter sido públicos. Depois, qualquer reorganização de diretórios vira uma alteração incompatível. Com exports, o autor estabelece um contrato claro: somente os caminhos declarados podem ser usados. Com imports, o próprio pacote cria aliases privados, sempre iniciados por #, para organizar melhor o código interno.
Por que usar exports no package.json
O campo main define apenas uma entrada principal. Já exports pode declarar a entrada principal, subcaminhos, condições para ESM e CommonJS, versões específicas para Node.js e alternativas para outros ambientes.
{
"name": "minha-biblioteca",
"type": "module",
"main": "./dist/index.js",
"exports": {
".": "./dist/index.js",
"./cliente": "./dist/cliente.js",
"./servidor": "./dist/servidor.js"
}
}Com essa configuração, os consumidores podem importar a raiz, minha-biblioteca/cliente e minha-biblioteca/servidor. Qualquer tentativa de acessar outro arquivo interno gera ERR_PACKAGE_PATH_NOT_EXPORTED. Isso protege a estrutura interna e permite refatorações sem quebrar aplicações que respeitam a interface documentada.
Encapsulamento e compatibilidade
Adicionar exports a um pacote antigo pode ser uma alteração incompatível. Antes da mudança, usuários talvez importassem caminhos como pacote/lib/utils.js. Depois da ativação, esses caminhos deixam de funcionar se não forem listados.
Uma migração segura começa com um inventário dos caminhos realmente consumidos. É possível declarar temporariamente os caminhos antigos e removê-los apenas em uma versão principal futura.
{
"exports": {
".": "./dist/index.js",
"./utils": "./dist/utils.js",
"./lib/utils.js": "./dist/utils.js",
"./package.json": "./package.json"
}
}Exportar o próprio package.json deve ser uma decisão consciente. Sem a entrada ./package.json, consumidores não podem mais carregá-lo pelo nome do pacote. Isso costuma ser desejável, mas pode quebrar ferramentas legadas.
Subpath exports
Subpath exports dividem uma biblioteca em entradas menores. Em vez de importar um módulo gigantesco, o consumidor acessa somente a funcionalidade necessária.
import { criarCliente } from 'minha-biblioteca/cliente';
import { iniciarServidor } from 'minha-biblioteca/servidor';Essa separação melhora a clareza e pode ajudar ferramentas de empacotamento. Entretanto, cada subcaminho publicado vira parte do contrato semântico do pacote. Por isso, use nomes estáveis e evite expor diretórios internos diretamente.
Em bibliotecas com muitos módulos, padrões podem reduzir repetição:
{
"exports": {
".": "./dist/index.js",
"./features/*.js": "./dist/features/*.js",
"./features/private/*": null
}
}O valor null bloqueia uma área específica mesmo quando um padrão mais amplo a alcançaria.
Extensões explícitas ou omitidas
O autor deve escolher um estilo consistente. É possível publicar pacote/recurso ou pacote/recurso.js. Extensões explícitas combinam melhor com a resolução nativa de módulos ES e com import maps. Caminhos sem extensão costumam ser mais curtos e escondem o formato físico do arquivo.
Evite oferecer os dois estilos para o mesmo módulo, pois isso cria duas formas públicas de acessar o mesmo recurso. Um contrato único simplifica documentação, autocomplete e manutenção.
Conditional exports para ESM e CommonJS
Pacotes que precisam atender import e require podem usar condições:
{
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"default": "./dist/index.js"
}
}
}A ordem das condições importa. Condições específicas devem aparecer antes de default. O arquivo CommonJS precisa ser carregável por require(), e o arquivo ESM deve respeitar a semântica de módulos ES.
Pacotes duais exigem atenção ao chamado dual package hazard: a mesma biblioteca pode ser carregada duas vezes, uma por ESM e outra por CommonJS, criando estados independentes. Isso é especialmente problemático em singletons, caches e registradores globais. Sempre que possível, mantenha o estado compartilhado em uma camada comum ou publique uma implementação principal única.
Condições node, default e types
Além de import e require, condições como node, default e types permitem adaptar a resolução.
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"node": "./dist/index.js",
"default": "./dist/index-universal.js"
}
}
}A condição types deve ficar no início para ferramentas de tipagem. A condição default deve ficar por último como fallback. Evite depender de condições personalizadas sem documentá-las, porque consumidores e bundlers podem não reconhecê-las.
Como usar imports para aliases privados
O campo imports funciona apenas dentro do próprio pacote. As chaves precisam começar com #.
{
"imports": {
"#config": "./src/config.js",
"#db/*": "./src/database/*.js",
"#logger": {
"development": "./src/logger-dev.js",
"default": "./src/logger.js"
}
}
}O código interno pode então importar módulos sem caminhos relativos profundos:
import config from '#config';
import { buscarUsuario } from '#db/usuarios.js';
import logger from '#logger';Isso reduz acoplamento à localização física dos arquivos. Se a pasta database mudar, basta alterar o mapa.
Diferença entre exports e imports
exports define o que consumidores externos podem acessar. imports define atalhos privados usados pelo próprio pacote. O primeiro protege a API pública; o segundo organiza dependências internas.
Também existe uma diferença importante: destinos em exports precisam apontar para caminhos relativos dentro do pacote. Já imports pode mapear uma chave privada para outro pacote externo.
{
"imports": {
"#hash": {
"node": "node:crypto",
"default": "hash-wasm"
}
}
}Regras de segurança dos caminhos
Alvos de exports devem começar com ./. Caminhos absolutos, URLs de arquivo e travessias com .. são rejeitados. Segmentos envolvendo node_modules também não são permitidos. Essas restrições impedem que o mapa exponha arquivos fora da raiz do pacote.
Considere o mapa como uma lista de permissões. Exponha somente o necessário e mantenha módulos auxiliares privados.
Self-reference dentro do próprio pacote
Quando name e exports estão definidos, arquivos internos podem importar o próprio pacote pelo nome:
{
"name": "@empresa/sdk",
"exports": {
".": "./src/index.js",
"./http": "./src/http.js"
}
}import { request } from '@empresa/sdk/http';Essa técnica garante que o código interno use a mesma API pública entregue aos consumidores. Porém, ela também impede acesso a caminhos não exportados, o que é útil para detectar violações do contrato durante o desenvolvimento.
Testes essenciais
Teste a biblioteca como um consumidor real. Empacote com npm pack, instale o arquivo gerado em um projeto temporário e valide importações ESM e CommonJS.
npm pack
mkdir /tmp/teste-pacote
cd /tmp/teste-pacote
npm init -y
npm install /caminho/minha-biblioteca-1.0.0.tgzCrie testes para cada entrada pública, verifique erros esperados em caminhos privados e confirme que os arquivos de tipos são encontrados. Automatize isso no CI.
Estratégia recomendada
Comece com uma API pequena. Declare type explicitamente, mantenha main apenas quando precisar de compatibilidade, use exports como fonte principal e adicione imports para aliases internos estáveis. Antes de publicar, valide o pacote empacotado e trate qualquer alteração na lista de exports como mudança potencialmente incompatível.
Os campos não eliminam a necessidade de versionamento semântico, mas tornam a fronteira pública visível e testável. Eles combinam especialmente bem com uma estratégia de Single Executable no Node.js, uma configuração consistente de NestJS no Node.js, builds com tRPC no Node.js e pipelines de GitHub Container Registry.
Para detalhes normativos, consulte a documentação oficial de pacotes do Node.js e a referência oficial do package.json no npm.



