Os campos exports e imports no package.json definem a interface pública e os aliases privados de um pacote Node.js. Com exports, o autor escolhe quais caminhos consumidores podem importar. Com imports, o próprio pacote cria atalhos internos iniciados por #, evitando caminhos relativos longos.
Esses campos melhoram encapsulamento, permitem múltiplos entry points e oferecem condições diferentes para import, require, Node.js e outros ambientes. Porém, adicionar exports a um pacote existente pode ser uma mudança incompatível: qualquer subpath não listado deixa de funcionar.
Neste guia, você aprenderá a configurar entry points, subpaths, patterns, condições, tipos, self-reference, aliases internos e compatibilidade entre ESM e CommonJS.
main versus exports
O campo main define apenas o entry point principal:
{
"main": "./dist/index.js"
}O campo exports é mais moderno e tem precedência nas versões compatíveis:
{
"main": "./dist/index.js",
"exports": "./dist/index.js"
}A documentação oficial de pacotes do Node.js recomenda exports para novos pacotes direcionados a versões suportadas.
Encapsulamento
Quando exports existe, caminhos não definidos falham:
import packageApi from 'meu-pacote'; // funciona
import internal from 'meu-pacote/src/internal.js'; // falhaIsso permite reorganizar arquivos internos sem quebrar consumidores que respeitam a API pública.
Entry point principal
{
"name": "@empresa/domain",
"type": "module",
"exports": "./dist/index.js"
}A forma expandida é equivalente:
{
"exports": {
".": "./dist/index.js"
}
}Subpath exports
{
"exports": {
".": "./dist/index.js",
"./errors": "./dist/errors.js",
"./testing": "./dist/testing.js"
}
}Consumidores usam:
import { DomainError } from '@empresa/domain/errors';
import { fakeOrder } from '@empresa/domain/testing';Escolha uma convenção com extensão ou sem extensão e mantenha consistência.
Targets relativos
Os valores precisam começar com ./:
{
"exports": {
".": "./dist/index.js"
}
}Caminhos absolutos, ../ e travessia para fora do pacote não são permitidos.
Patterns
Pacotes com muitos subpaths podem usar patterns:
{
"exports": {
"./features/*.js": "./dist/features/*.js"
}
}import feature from 'meu-pacote/features/search.js';O asterisco é uma substituição textual. Exponha apenas o necessário; patterns amplos podem transformar internals em contrato público.
Bloqueando um subdiretório
{
"exports": {
"./features/*.js": "./dist/features/*.js",
"./features/private/*": null
}
}O target null bloqueia explicitamente a região privada.
Exportando package.json
Se consumidores precisam ler metadados:
{
"exports": {
".": "./dist/index.js",
"./package.json": "./package.json"
}
}Não exponha por hábito. Trate esse caminho como parte da API.
Conditional exports
{
"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.
Risco de pacote dual
Oferecer implementações ESM e CommonJS pode carregar duas instâncias do mesmo pacote no mesmo processo, causando diferenças em singletons, classes e caches. Quando possível, publique uma única implementação ESM e avalie a compatibilidade exigida.
Consulte ES Modules no Node.js e CommonJS no Node.js.
Condição node e default
{
"exports": {
".": {
"node": "./dist/node.js",
"default": "./dist/universal.js"
}
}
}Uma condição default ajuda runtimes desconhecidos sem fazê-los fingir ser outro ambiente.
Condição types
Ferramentas de tipos podem usar:
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}types deve aparecer antes de outras condições. Também mantenha o campo types principal quando necessário para compatibilidade.
Subpaths com tipos
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./errors": {
"types": "./dist/errors.d.ts",
"default": "./dist/errors.js"
}
}
}Campo imports
imports cria mappings privados usados apenas dentro do pacote. As chaves começam com #:
{
"imports": {
"#config": "./dist/config.js",
"#domain/*": "./dist/domain/*.js"
}
}import config from '#config';
import Order from '#domain/order.js';Por que usar imports?
Sem aliases:
import config from '../../../../config/index.js';Com mapping:
import config from '#config';O alias é resolvido pelo Node.js e não depende apenas do editor ou bundler.
Imports condicionais
{
"imports": {
"#database": {
"development": "./dist/database-memory.js",
"default": "./dist/database-postgres.js"
}
}
}Execute com condição customizada:
node --conditions=development dist/server.jsUse condições com cuidado; configuração invisível pode dificultar diagnóstico.
Imports para pacote externo
Diferente de exports, imports pode apontar para dependência externa:
{
"imports": {
"#fetch": {
"node": "undici",
"default": "./dist/fetch-polyfill.js"
}
},
"dependencies": {
"undici": "^7.0.0"
}
}Self-reference
Um pacote com name e exports pode importar sua própria API:
{
"name": "@empresa/domain",
"exports": {
".": "./dist/index.js",
"./errors": "./dist/errors.js"
}
}import { DomainError } from '@empresa/domain/errors';Self-reference força o código interno a respeitar os mesmos exports dos consumidores.
Workspaces
Em monorepos, cada workspace deve possuir exports próprio. Isso evita imports diretos em src e preserva fronteiras.
Veja npm Workspaces no Node.js.
TypeScript NodeNext
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}Essa configuração aproxima a resolução do TypeScript às regras do Node.js, incluindo type, extensões e exports.
Build alinhado
Se exports aponta para dist/index.js, o build precisa gerar exatamente esse arquivo e sua declaração. Adicione teste que empacota e importa o pacote.
npm pack
npm pack --dry-runConfirme que todos os targets de exports estão no pacote e que internals não foram incluídos desnecessariamente.
Teste da API pública
import assert from 'node:assert/strict';
import { createOrder } from '@empresa/domain';
import { DomainError } from '@empresa/domain/errors';
assert.equal(typeof createOrder, 'function');
assert.equal(typeof DomainError, 'function');Execute contra o tarball instalado em um projeto temporário para validar resolução real.
Migrando pacote existente
Adicionar exports bloqueia deep imports. Antes:
- procure caminhos usados por consumidores;
- liste subpaths suportados;
- adicione aliases de compatibilidade;
- publique warning de depreciação;
- restrinja internals em versão major;
- documente todos os entry points.
Semver
Remover ou alterar um export público é breaking change. Adicionar um subpath normalmente é compatível, mas mudanças de tipos e condições podem afetar consumidores.
Extensions ou extensionless?
O Node.js recomenda escolher um único estilo. Subpaths com extensão combinam melhor com import maps e deixam o specifier explícito. Extensionless pode ser mais curto. Não ofereça ambos sem necessidade, porque cria duas formas de importar a mesma API.
Segurança
exports melhora encapsulamento, mas não é uma barreira de segurança absoluta. Um consumidor com caminho absoluto ainda pode acessar arquivos presentes. Use files no package.json para não publicar secrets, testes ou fontes privadas.
Erros comuns
- Adicionar exports em patch: deep imports quebram.
- Default primeiro: condições específicas nunca são usadas.
- Target sem ./: configuração inválida.
- Pattern amplo: internals viram API.
- Tipos ausentes: runtime funciona e editor falha.
- Build divergente: target não existe no tarball.
- Dual package: duas instâncias causam inconsistência.
- Alias só no TypeScript: Node.js não resolve em runtime.
Exemplo recomendado
{
"name": "@empresa/domain",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./errors": {
"types": "./dist/errors.d.ts",
"default": "./dist/errors.js"
},
"./package.json": "./package.json"
},
"imports": {
"#internal/*": "./dist/internal/*.js"
},
"files": ["dist", "README.md", "LICENSE"]
}Conclusão
Os campos exports e imports no package.json transformam a resolução de módulos em um contrato explícito. exports define o que consumidores podem usar; imports organiza aliases privados e condições internas.
Liste entry points pequenos, inclua tipos, teste o tarball e trate qualquer remoção como breaking change. Com exports bem definidos, pacotes Node.js ganham encapsulamento e podem evoluir a estrutura interna sem depender de caminhos acidentais.




