Package Exports no Node.js é o mecanismo usado para definir quais caminhos de um pacote podem ser importados por aplicações consumidoras. O campo exports do package.json funciona como uma fronteira pública: ele libera entradas suportadas e bloqueia o acesso acidental a arquivos internos.
Sem essa fronteira, consumidores podem depender de qualquer arquivo presente no pacote. Uma reorganização interna, mesmo sem alterar a API pretendida, passa a causar quebras. Com exports, o mantenedor controla a superfície pública, cria subpaths estáveis e prepara compatibilidade entre ESM e CommonJS.
Neste guia, você aprenderá entrada principal, subpath exports, padrões, encapsulamento, tipos TypeScript, compatibilidade, testes e migração segura.
Por que usar o campo exports?
Um pacote tradicional pode definir apenas main:
{
"name": "minha-lib",
"main": "./dist/index.js"
}Isso informa a entrada principal, mas não impede imports internos:
const helper = require('minha-lib/dist/internal/helper.js');Quando exports está presente, apenas os caminhos declarados ficam disponíveis.
Export principal
{
"name": "minha-lib",
"type": "module",
"exports": "./dist/index.js"
}O consumidor usa:
import { criarCliente } from 'minha-lib';Formato com ponto
Quando haverá mais de uma entrada, use um objeto:
{
"exports": {
".": "./dist/index.js",
"./errors": "./dist/errors.js",
"./testing": "./dist/testing.js"
}
}Agora estes imports são públicos:
import { criarCliente } from 'minha-lib';
import { AppError } from 'minha-lib/errors';
import { criarFake } from 'minha-lib/testing';Encapsulamento de arquivos internos
Um import não declarado gera erro, mesmo que o arquivo exista fisicamente no pacote. Isso permite mover implementações internas sem transformar cada arquivo em compromisso público.
O encapsulamento não é uma barreira de segurança contra acesso ao disco. Ele é um contrato de resolução de módulos.
Subpaths pequenos e estáveis
Evite exportar centenas de arquivos individuais. Prefira entradas orientadas a capacidades:
{
"exports": {
".": "./dist/index.js",
"./http": "./dist/http/index.js",
"./database": "./dist/database/index.js",
"./errors": "./dist/errors/index.js"
}
}Cada subpath deve ter finalidade clara e documentação própria.
Exportando package.json
Alguns consumidores precisam ler versão ou metadados:
{
"exports": {
".": "./dist/index.js",
"./package.json": "./package.json"
}
}Exponha somente quando houver necessidade. Não transforme arquivos administrativos em API por conveniência.
Padrões de subpath
{
"exports": {
"./features/*.js": "./dist/features/*.js"
}
}Padrões reduzem repetição, mas ampliam a superfície pública. Para bibliotecas pequenas e médias, declarações explícitas são mais fáceis de versionar.
Bloqueando caminhos específicos
{
"exports": {
".": "./dist/index.js",
"./features/*.js": "./dist/features/*.js",
"./features/private/*": null
}
}O valor null torna a intenção explícita, mas uma API pública menor continua sendo a melhor estratégia.
Exports e ESM
Um pacote ESM pode declarar:
{
"type": "module",
"exports": {
".": "./dist/index.js"
}
}Veja ESM no Node.js para entender extensões, import.meta.url e migração.
Exports condicionais
É possível entregar arquivos diferentes conforme o método de carregamento:
{
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}A ordem e a compatibilidade exigem cuidado. O artigo Conditional Exports no Node.js aprofunda essa configuração.
Tipos TypeScript
Os tipos precisam acompanhar cada entrada pública. Uma configuração simples:
{
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./errors": {
"types": "./dist/errors.d.ts",
"import": "./dist/errors.js"
}
}
}Teste o pacote em um projeto consumidor real. Ferramentas e versões podem interpretar metadados de formas diferentes.
O problema do dual package hazard
Quando um pacote fornece ESM e CommonJS, a mesma aplicação pode carregar duas instâncias da biblioteca. Estado singleton, classes e símbolos podem deixar de ser compartilhados.
// duas árvores de carregamento podem manter caches diferentes
import lib from 'minha-lib';
const libCjs = require('minha-lib');Evite estado global e teste a combinação de consumidores.
Compatibilidade com main
Alguns pacotes mantêm main para ferramentas antigas:
{
"main": "./dist/index.cjs",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}Para runtimes que entendem exports, esse campo tem prioridade. Documente a versão mínima suportada.
Imports internos do próprio pacote
O campo imports cria aliases privados iniciados por #:
{
"imports": {
"#config": "./src/config.js",
"#logger": "./src/logger.js"
}
}import { logger } from '#logger';Esses aliases não são exportados aos consumidores. Eles ajudam a evitar caminhos relativos profundos sem publicar detalhes internos.
Estrutura recomendada
src/
index.js
errors.js
http/
index.js
internal/
normalize.js
dist/
index.js
errors.js
http/
index.jsSomente index, errors e http devem aparecer em exports. A pasta internal permanece privada.
Testando a API pública
Crie um teste que empacota e instala a biblioteca em diretório temporário:
npm pack
mkdir consumer-test
cd consumer-test
npm init -y
npm install ../minha-lib-1.0.0.tgzVerifique imports permitidos e bloqueados, tipos, ESM, CommonJS e subpaths.
Teste de caminhos não autorizados
import assert from 'node:assert/strict';
await assert.rejects(
import('minha-lib/dist/internal/helper.js')
);Esse teste protege o encapsulamento durante mudanças no build.
Publicação e arquivos incluídos
O campo exports não adiciona arquivos ao pacote. Use files e confira o resultado de npm pack --dry-run:
{
"files": [
"dist",
"README.md",
"LICENSE"
]
}Garanta que todos os alvos declarados realmente sejam publicados.
Migração para exports
- liste os imports usados por consumidores;
- identifique caminhos internos adotados sem intenção;
- crie entradas públicas equivalentes;
- adicione exports primeiro em versão de teste;
- execute testes de consumidores;
- publique mudança incompatível em versão major quando necessário;
- documente substituições para caminhos bloqueados;
- monitore issues após a publicação.
Semantic Versioning
Remover ou renomear um subpath público é breaking change. Adicionar novo subpath normalmente é compatível, desde que não altere resolução existente.
Não trate a estrutura interna do pacote como detalhe depois que ela foi usada publicamente sem exports. Faça uma migração explícita.
Erros comuns
- declarar alvo que não entra no pacote;
- esquecer extensões nos arquivos ESM;
- exportar toda a pasta dist;
- manter tipos apenas para a entrada principal;
- criar duas instâncias com ESM e CommonJS;
- alterar subpath sem versão major;
- depender de ordem condicional sem testes;
- confundir encapsulamento com segurança do sistema de arquivos.
Checklist para produção
- defina uma API pública pequena;
- declare ponto principal com
.; - adicione somente subpaths necessários;
- publique todos os arquivos-alvo;
- inclua tipos;
- teste ESM e CommonJS;
- instale o tarball em projeto consumidor;
- bloqueie imports internos;
- versione alterações incompatíveis.
Conclusão
Package Exports no Node.js transforma a estrutura de um pacote em contrato explícito. O mantenedor deixa de expor acidentalmente toda a pasta publicada e passa a controlar entradas, subpaths, tipos e compatibilidade.
Comece com uma superfície pequena, teste o pacote empacotado e trate cada caminho exportado como API de longo prazo. Essa disciplina reduz quebras e permite reorganizar a implementação com confiança.
Consulte a documentação oficial de pontos de entrada de pacotes do Node.js e a documentação do package.json no npm.



