The node:module API in Node.js

The node:module API provides general utility methods for interacting with Node.js modules. It allows developers to inspect built-in modules, create require functions inside ES modules, locate package.json files, register custom hooks to modify module resolution and loading behavior, synchronize CommonJS and ESM exports, and enable compile caching to improve performance.

module.builtinModulesmodule.createRequiremodule.findPackageJSONmodule.isBuiltinmodule.register / module.registerHooks

~10 min read • Updated Dec 29, 2025

1. Introduction


The node:module API was introduced in v0.3.7. It provides utilities for managing modules and can be accessed via require('node:module') or import 'node:module'.


2. module.builtinModules


Returns a list of all built-in Node.js modules. Useful for distinguishing between built-in and third-party modules.


3. module.createRequire(filename)


Creates a require function inside ES modules:


import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const siblingModule = require('./sibling-module');

4. module.findPackageJSON(specifier[, base])


Finds the closest package.json file for a given module specifier. Useful for locating package roots or nested package.json files.


5. module.isBuiltin(moduleName)


Checks if a module is built-in:


import { isBuiltin } from 'node:module';
isBuiltin('fs'); // true
isBuiltin('wss'); // false

6. module.register and module.registerHooks


  • module.register: Registers asynchronous hooks for customizing module resolution and loading.
  • module.registerHooks: Registers synchronous hooks for customizing resolution and loading.

7. module.stripTypeScriptTypes


Removes TypeScript type annotations from code. In transform mode, it also converts TypeScript features into JavaScript.


const { stripTypeScriptTypes } = require('node:module');
const code = 'const a: number = 1;';
console.log(stripTypeScriptTypes(code));
// Output: const a = 1;

8. module.syncBuiltinESMExports()


Synchronizes live bindings of built-in ES modules with CommonJS exports. It updates values but does not add or remove export names.


9. Compile Cache


Node.js can persist V8 compile caches to disk to speed up subsequent module loads. Enabled via module.enableCompileCache() or the NODE_COMPILE_CACHE environment variable.


  • Portable mode allows caches to be reused even if the project directory is moved.
  • module.flushCompileCache() forces cache flushing to disk.
  • module.getCompileCacheDir() returns the active cache directory.

10. Compile Cache Status


Results of enabling compile cache are indicated by constants such as ENABLED, ALREADY_ENABLED, FAILED, or DISABLED.


Conclusion


The node:module API provides powerful tools for module management, customization, and performance optimization. With features like custom hooks, TypeScript stripping, and compile caching, developers gain fine-grained control over how modules are resolved, loaded, and executed in Node.js.


1. Enabling Hooks


Hooks can be registered in two ways:


  • module.register(): Registers asynchronous hooks in a dedicated thread.
  • module.registerHooks(): Registers synchronous hooks in the main thread.

Hooks can be enabled before application code runs using:


node --import ./register-hooks.js ./my-app.js
node --require ./register-hooks.js ./my-app.js

2. Example


// register-hooks.js
const { register } = require('node:module');
const { pathToFileURL } = require('node:url');

// Asynchronous hooks
register('./hooks.mjs', pathToFileURL(__filename));

// Synchronous hooks
const { registerHooks } = require('node:module');
registerHooks({
  resolve(specifier, context, nextResolve) { /* implementation */ },
  load(url, context, nextLoad) { /* implementation */ },
});

3. Using Dependencies


Hooks can also be registered from a dependency:


node --import some-package/register ./my-app.js
node --require some-package/register ./my-app.js

4. Dynamic vs Static Import


  • Dynamic import() ensures hooks are applied before modules load.
  • Static import may load modules before hooks are registered.

5. Support for require and createRequire


Synchronous hooks affect import, require(), and user-defined require() created with createRequire().


6. Chaining Hooks


Multiple hooks can be chained. They execute in LIFO order:


// entrypoint.cjs
const { register } = require('node:module');
const { pathToFileURL } = require('node:url');

const parentURL = pathToFileURL(__filename);
register('./foo.mjs', parentURL);
register('./bar.mjs', parentURL);
import('./my-app.mjs');

Here, bar.mjs runs first, then foo.mjs, followed by Node.js defaults.


7. Communication with Asynchronous Hooks


Asynchronous hooks run in separate threads, requiring MessageChannel for communication:


const { register } = require('node:module');
const { pathToFileURL } = require('node:url');
const { MessageChannel } = require('node:worker_threads');

const { port1, port2 } = new MessageChannel();
port1.on('message', (msg) => console.log(msg));
port1.unref();

register('./my-hooks.mjs', {
  parentURL: pathToFileURL(__filename),
  data: { number: 1, port: port2 },
  transferList: [port2],
});

Conclusion


Customization hooks in Node.js provide powerful control over module resolution and loading. With synchronous and asynchronous options, chaining, and communication support, developers can tailor module behavior to suit complex application needs.


1. Asynchronous Hooks


Registered with module.register(), asynchronous hooks run in a separate thread:


export async function initialize({ number, port }) {
  // Receives data from register
}

export async function resolve(specifier, context, nextResolve) {
  // Resolve an import or require specifier to a URL
}

export async function load(url, context, nextLoad) {
  // Load source code from the resolved URL
}

These hooks are inherited into child worker threads by default.


2. Synchronous Hooks


Registered with module.registerHooks(), synchronous hooks run in the main thread:


function resolve(specifier, context, nextResolve) {
  // Resolve specifier to a URL
}

function load(url, context, nextLoad) {
  // Load source code from the resolved URL
}

Unlike asynchronous hooks, they are not inherited into child workers unless preloaded with --import or --require.


3. Hook Conventions


  • Hooks are always part of a chain, even if only one custom hook is present.
  • Each hook must return a plain object.
  • To intentionally end the chain, return shortCircuit: true.

4. initialize()


Supported only by register(), the initialize hook runs in the hooks thread when the module is registered. It can receive data and transferable objects:


// path-to-my-hooks.js
export async function initialize({ number, port }) {
  port.postMessage(`increment: ${number + 1}`);
}

5. Communication with MessageChannel


Asynchronous hooks require message channels for communication with the main thread:


const { register } = require('node:module');
const { pathToFileURL } = require('node:url');
const { MessageChannel } = require('node:worker_threads');

const { port1, port2 } = new MessageChannel();
port1.on('message', (msg) => console.log(msg));
port1.unref();

register('./path-to-my-hooks.js', {
  parentURL: pathToFileURL(__filename),
  data: { number: 1, port: port2 },
  transferList: [port2],
});

6. resolve()


The resolve hook determines the final URL for a module specifier. It can also provide a format hint to the load hook:


// Asynchronous
export async function resolve(specifier, context, nextResolve) {
  return nextResolve(specifier, {
    ...context,
    conditions: [...context.conditions, 'another-condition'],
  });
}

// Synchronous
function resolve(specifier, context, nextResolve) {
  return nextResolve(specifier);
}

Conclusion


Hooks in Node.js give developers fine-grained control over module resolution and loading. With initialize, resolve, and load, developers can override defaults, chain custom logic, and manage communication between threads, making the module system highly extensible.


1. Definition


load(url, context, nextLoad) accepts:


  • url: The resolved module URL.
  • context: Includes conditions, format, and import attributes.
  • nextLoad: The next hook in the chain or Node.js default loader.

It must return an object with format and source.


2. Supported Formats


FormatDescriptionSource Types
addonNode.js addonnull
builtinBuilt-in modulenull
commonjsCommonJS modulestring / ArrayBuffer / TypedArray
jsonJSON filestring / ArrayBuffer / TypedArray
moduleES modulestring / ArrayBuffer / TypedArray
wasmWebAssembly moduleArrayBuffer / TypedArray

3. Asynchronous vs Synchronous


  • Asynchronous: Runs in a separate thread, can return a Promise.
  • Synchronous: Runs in the main thread, returns synchronously.

For CommonJS, providing a source in async mode routes require calls through the ESM loader. If source is null, the legacy CommonJS loader is used.


4. Example


// Asynchronous
export async function load(url, context, nextLoad) {
  const { format } = context;
  if (Math.random() > 0.5) {
    return { format, shortCircuit: true, source: '...' };
  }
  return nextLoad(url);
}

// Synchronous
function load(url, context, nextLoad) {
  return nextLoad(url);
}

5. HTTPS Imports


Hooks can fetch modules over the network:


// https-hooks.mjs
import { get } from 'node:https';

export function load(url, context, nextLoad) {
  if (url.startsWith('https://')) {
    return new Promise((resolve, reject) => {
      get(url, (res) => {
        let data = '';
        res.setEncoding('utf8');
        res.on('data', (chunk) => data += chunk);
        res.on('end', () => resolve({
          format: 'module',
          shortCircuit: true,
          source: data,
        }));
      }).on('error', reject);
    });
  }
  return nextLoad(url);
}

6. Key Notes


  • Async load hooks are incompatible with CommonJS named exports.
  • Text-based formats use TextDecoder if source is not a string.
  • Return shortCircuit: true to end the chain intentionally.

Conclusion


The load hook in Node.js provides developers with powerful control over how modules are loaded. It enables loading from disk, network, or custom formats, and allows transformation of unsupported sources. This flexibility makes the module system highly extensible for advanced use cases.


1. Transpilation with Load Hooks


Sources in formats not natively understood by Node.js can be converted into JavaScript using the load hook. This allows Node.js to execute CoffeeScript or other custom formats.


Asynchronous Example


// coffeescript-hooks.mjs
import { readFile } from 'node:fs/promises';
import { findPackageJSON } from 'node:module';
import coffeescript from 'coffeescript';

const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/;

export async function load(url, context, nextLoad) {
  if (extensionsRegex.test(url)) {
    const { source: rawSource } = await nextLoad(url, { ...context, format: 'coffee' });
    const transformedSource = coffeescript.compile(rawSource.toString(), url);
    return {
      format: await getPackageType(url),
      shortCircuit: true,
      source: transformedSource,
    };
  }
  return nextLoad(url, context);
}

Synchronous Example


// coffeescript-sync-hooks.mjs
import { readFileSync } from 'node:fs';
import { registerHooks, findPackageJSON } from 'node:module';
import coffeescript from 'coffeescript';

function load(url, context, nextLoad) {
  if (/\.(coffee|litcoffee|coffee\.md)$/.test(url)) {
    const { source: rawSource } = nextLoad(url, { ...context, format: 'coffee' });
    const transformedSource = coffeescript.compile(rawSource.toString(), url);
    return { format: getPackageType(url), shortCircuit: true, source: transformedSource };
  }
  return nextLoad(url, context);
}

registerHooks({ load });

2. Running CoffeeScript Hooks


With a package.json specifying "type": "module", CoffeeScript files can be imported and transpiled on the fly:


// main.coffee
import { scream } from './scream.coffee'
console.log scream 'hello, world'

3. Import Maps with Resolve Hooks


Import maps allow developers to override specifiers to custom URLs. This can be implemented with resolve hooks.


Asynchronous Example


// import-map-hooks.js
import fs from 'node:fs/promises';
const { imports } = JSON.parse(await fs.readFile('import-map.json'));

export async function resolve(specifier, context, nextResolve) {
  if (Object.hasOwn(imports, specifier)) {
    return nextResolve(imports[specifier], context);
  }
  return nextResolve(specifier, context);
}

Synchronous Example


// import-map-sync-hooks.js
import fs from 'node:fs';
import module from 'node:module';

const { imports } = JSON.parse(fs.readFileSync('import-map.json', 'utf-8'));

function resolve(specifier, context, nextResolve) {
  if (Object.hasOwn(imports, specifier)) {
    return nextResolve(imports[specifier], context);
  }
  return nextResolve(specifier, context);
}

module.registerHooks({ resolve });

4. Source Map Support


Node.js supports TC39 ECMA-426 Source Map format for improved debugging. Source maps map compiled code back to original source files.


  • module.getSourceMapsSupport(): Checks if source map support is enabled.
  • module.findSourceMap(path): Retrieves a source map for a given file.
  • module.setSourceMapsSupport(enabled, options): Enables or disables source map support programmatically.

SourceMap Class


The SourceMap class allows developers to interact with source maps:


  • new SourceMap(payload): Creates a new source map instance.
  • findEntry(lineOffset, columnOffset): Finds mapping entries.
  • findOrigin(lineNumber, columnNumber): Maps back to original source.

Conclusion


Transpilation hooks, import maps, and source map support extend Node.js beyond its default capabilities. They enable developers to run non-standard formats, customize module resolution, and debug effectively with source maps. While powerful, these features should be used carefully, especially transpilation hooks, which are best suited for development and testing environments.


Written & researched by Dr. Shahin Siami