Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Exports e Imports no package.json

Atualizado em: 8 de setembro de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

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'; // falha

Isso 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.js

Use 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-run

Confirme 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:

  1. procure caminhos usados por consumidores;
  2. liste subpaths suportados;
  3. adicione aliases de compatibilidade;
  4. publique warning de depreciação;
  5. restrinja internals em versão major;
  6. 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.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita