Course contentsShow
← Node.js
Lesson 110 of 1,9802. Module Systems and Environment ConfigurationFree lesson

Creating Hybrid NPM Packages

Build a library that supports both ESM and CJS consumers simultaneously through dual-package patterns.

In this lesson, you will learn how to structure your library so that it can be imported by users utilizing either the older CommonJS (require) or the modern ESM (import) syntax.

The Hybrid Challenge

When you publish an NPM package, your consumers might be running different versions of Node.js or using different build tools. If you only provide CommonJS, ESM users can't benefit from static analysis or top-level await. If you only provide ESM, CommonJS users will face "require of ES module not supported" errors. A "Hybrid Package" bridges this gap by providing two versions of the same code.

The Dual-Package Pattern

The secret lies in the package.json file. By using the exports field, you can tell Node.js exactly which file to serve based on the consumer's environment.

Imagine your project has two folders: /dist/cjs and /dist/esm. You define your package.json like this:

{
  "name": "my-cool-library",
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js"
    }
  }
}

When a user writes import { x } from 'my-cool-library', Node.js looks at the import condition and maps it to your ESM build. When a user writes const { x } = require('my-cool-library'), Node.js uses the require condition to load your CommonJS build.

Why this works

Node.js processes the exports map before falling back to main. This allows you to maintain two separate versions of your source code—compiled once for CJS and once for ESM—while presenting a unified API to the user. By directing traffic at the resolution level, you avoid forcing the consumer to change their project settings or manually rename files. You are effectively acting as a translator, ensuring your logic is accessible regardless of how the user's runtime handles module resolution.

Key Takeaway: By using the exports field in package.json with conditional paths for import and require, you can deliver a seamless, compatible experience for every Node.js developer.