Skip to solution
hardBackend

What is the package.json "exports" field and conditional exports?

404 views
01

Understand the problem

Question presented to candidate: "A package you maintain needs to expose a different implementation to CommonJS consumers than to ESM consumers — and you also want to guarantee nobody reaches into your package's internal files directly. How do you express both of those requirements in package.json?"

What a strong answer should cover:

  • The "exports" field in package.json does two things at once, directly answering both halves of the prompt: it defines which paths of a package are importable at all (anything not listed is genuinely blocked), and — via conditional exports — it can map the same import path to different files depending on how the consumer is loading the package ("require" vs. "import", among other conditions).
  • 📌 Verified, not assumed — the exact answer to the prompt's first half: a real package with "exports": { ".": { "require": "./cjs-entry.js", "import": "./esm-entry.mjs" } } genuinely resolved to the require file when loaded via require(), and genuinely resolved to the different, import file when loaded via a real import() — the identical package specifier, genuinely different real files, chosen automatically based on how the consumer loaded it.
  • 📌 Verified, not assumed — the exact answer to the prompt's second half: a real file that genuinely existed on disk inside the package, but was not listed anywhere in "exports", was genuinely unreachablerequire()-ing it directly threw a real ERR_PACKAGE_PATH_NOT_EXPORTED, not merely a convention violated silently.
  • A precise answer contrasts this with the older "main" field: "main" only ever pointed to one entry file, with no conditional logic and no enforcement — any file inside the package's directory was always reachable via a deep import, regardless of whether the author intended that. "exports" is a genuinely stricter, more capable encapsulation boundary, verified directly above with a real blocked path.
  • The precise scope, stated honestly: conditions beyond "require"/"import" exist too ("node", "browser", "default" as a fallback, and custom conditions some tools define) — a package can layer several, evaluated in the order they're listed, with "default" conventionally last as the catch-all; this is the real mechanism behind what's sometimes called the "dual package hazard" workaround for shipping both a CJS and ESM build from one package.

Clarifying questions expected:

  • "Does this package genuinely need different CODE for CJS vs. ESM consumers, or just a different file extension/wrapper around identical logic?" — shapes whether conditional exports need real behavioral differences or just a thin compatibility shim.
  • "Are there existing consumers relying on deep imports into this package's internal files that a stricter "exports" field would now block?" — a real, breaking-change risk worth surfacing before introducing "exports" to an existing published package.

Code / implementation expected: Yes — a real package with genuine conditional exports (verified resolving differently for require vs. import) and a real blocked unlisted deep path is the concrete, convincing proof of exactly how both mechanisms work.

nodejsesmpackagingexports
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for Node.js package-authoring and module-system interviews — assumes familiarity with the "type": "module" question's ESM/CJS basics. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:</

Solution ready — 2 min read

Classified // press E to declassify

04

Read the code

A real package with conditional exports: genuine require-vs-import resolution, and a genuinely blocked unlisted path
// node_modules/my-lib/package.json
{
  "name": "my-lib",
  "exports": {
    ".": { "require": "./cjs-entry.js", "import": "./esm-entry.mjs" },
    "./internal/secret": "./should-not-be-reachable.js"
  }
}

// node_modules/my-lib/cjs-entry.js
module.exports = { via: "CJS conditional export" };

// node_modules/my-lib/esm-entry.mjs
export const via = "ESM conditional export";

// --- from a real CJS consumer ---
console.log(require("my-lib"));
// { via: 'CJS conditional export' }

// --- from a real ESM consumer ---
const { via } = await import("my-lib");
console.log(via);
// ESM conditional export

// --- reaching a real file on disk, NOT listed in "exports" ---
try {
  require("my-lib/deep-internal.js");
} catch (e) {
  console.log(e.code); // ERR_PACKAGE_PATH_NOT_EXPORTED — genuinely blocked
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 140 of 152 decoded in the Node.js track. One more won't hurt.

Back to track