Skip to main content
On this page

Module customization hooks

Deno supports the Node.js module.registerHooks() API, which lets you intercept and customize how modules are resolved and loaded. This enables virtual modules, custom transpilation, module aliasing, and similar use cases without modifying the importing code. The node:module API is part of Deno's broader Node.js compatibility layer.

The hooks are synchronous and run in the same thread as your application. They work for both ES modules (import) and CommonJS (require()).

Deno does not implement the asynchronous module.register() API. Use registerHooks() for both CommonJS and ESM customization.

Basic example Jump to heading

main.mjs
import { registerHooks } from "node:module";

const hooks = registerHooks({
  resolve(specifier, context, nextResolve) {
    if (specifier === "virtual:greet") {
      return { url: "file:///virtual_greet.js", shortCircuit: true };
    }
    return nextResolve(specifier, context);
  },
  load(url, context, nextLoad) {
    if (url === "file:///virtual_greet.js") {
      return {
        source: 'export const msg = "hello from hooks";',
        format: "module",
        shortCircuit: true,
      };
    }
    return nextLoad(url, context);
  },
});

const { msg } = await import("virtual:greet");
console.log(msg); // "hello from hooks"

// Remove hooks when no longer needed
hooks.deregister();
>_
deno run --allow-all main.mjs

Loading hooks with --import Jump to heading

To keep your application code clean — and to make sure the hooks are installed before anything in your program imports the modules they affect — put the registerHooks() call in its own loader file and preload it with --import (an alias for --preload).

loader.mjs
import { registerHooks } from "node:module";

registerHooks({
  resolve(specifier, context, nextResolve) {
    if (specifier === "virtual:greet") {
      return { url: "file:///virtual_greet.js", shortCircuit: true };
    }
    return nextResolve(specifier, context);
  },
  load(url, context, nextLoad) {
    if (url === "file:///virtual_greet.js") {
      return {
        source: 'export const msg = "hello from loader";',
        format: "module",
        shortCircuit: true,
      };
    }
    return nextLoad(url, context);
  },
});
main.mjs
const { msg } = await import("virtual:greet");
console.log(msg); // "hello from loader"

Run with --import pointing at the loader:

>_
deno run --import ./loader.mjs main.mjs

--import accepts multiple values, so you can compose loaders (e.g. --import ./aliases.mjs --import ./transpile.mjs). They register in the order given, which is the reverse of the order in which they run — see Hook chaining. The flag is available on deno run, deno test, deno bench, and deno serve.

Use cases Jump to heading

Custom transpilation Jump to heading

Transform non-standard file formats on the fly:

import { registerHooks } from "node:module";

registerHooks({
  load(url, context, nextLoad) {
    if (url.endsWith(".coffee")) {
      const result = nextLoad(url, context);
      const compiled = compileCoffeeScript(result.source);
      return { source: compiled, format: "module", shortCircuit: true };
    }
    return nextLoad(url, context);
  },
});

Module aliasing Jump to heading

Redirect imports to different modules:

import { registerHooks } from "node:module";

registerHooks({
  resolve(specifier, context, nextResolve) {
    // Redirect lodash to lodash-es
    if (specifier === "lodash") {
      return nextResolve("lodash-es", context);
    }
    return nextResolve(specifier, context);
  },
});

Virtual modules Jump to heading

Create modules that exist only in memory:

import { registerHooks } from "node:module";

const virtualModules = new Map([
  ["virtual:config", 'export default { debug: true, version: "1.0.0" };'],
  ["virtual:env", `export const NODE_ENV = "${process.env.NODE_ENV}";`],
]);

registerHooks({
  resolve(specifier, context, nextResolve) {
    if (virtualModules.has(specifier)) {
      return { url: `file:///virtual/${specifier}`, shortCircuit: true };
    }
    return nextResolve(specifier, context);
  },
  load(url, context, nextLoad) {
    for (const [name, source] of virtualModules) {
      if (url === `file:///virtual/${name}`) {
        return { source, format: "module", shortCircuit: true };
      }
    }
    return nextLoad(url, context);
  },
});

Mocking for tests Jump to heading

Replace modules with mocks during testing:

import { registerHooks } from "node:module";

const hooks = registerHooks({
  resolve(specifier, context, nextResolve) {
    if (specifier === "./database.js") {
      return { url: "file:///mock_database.js", shortCircuit: true };
    }
    return nextResolve(specifier, context);
  },
  load(url, context, nextLoad) {
    if (url === "file:///mock_database.js") {
      return {
        source: 'export const query = () => [{ id: 1, name: "mock" }];',
        format: "module",
        shortCircuit: true,
      };
    }
    return nextLoad(url, context);
  },
});

// Run tests...

hooks.deregister(); // Clean up after tests

The resolve hook Jump to heading

The resolve hook intercepts module resolution, mapping specifiers to URLs.

resolve(specifier, context, nextResolve);

Parameters:

Parameter Type Description
specifier string The module specifier being resolved
context object Resolution context (see below)
nextResolve function Delegates to the next hook or the default resolver

Context object:

Property Type Description
conditions string[] Import conditions (e.g., ["node", "import"] for ESM)
parentURL string URL of the importing module
importAttributes object Import attributes from the import statement

Return value:

Property Type Description
url string The resolved URL for the module
shortCircuit boolean If true, skip remaining hooks in the chain

Either call nextResolve() to delegate, or return a result with shortCircuit: true. You must do one or the other.

The load hook Jump to heading

The load hook intercepts module loading, providing the source code for a resolved URL.

load(url, context, nextLoad);

Parameters:

Parameter Type Description
url string The resolved module URL
context object Load context (see below)
nextLoad function Delegates to the next hook or the default loader

Context object:

Property Type Description
format string Module format hint (e.g., "module", "commonjs")
conditions string[] Import conditions
importAttributes object Import attributes

Return value:

Property Type Description
source string | Buffer | null The module source code
format string Module format: "module", "commonjs", "json"
shortCircuit boolean If true, skip remaining hooks in the chain

Deregistering hooks Jump to heading

registerHooks() returns an object with a deregister() method to remove the hooks:

const hooks = registerHooks({/* ... */});

// Later, remove hooks
hooks.deregister();

Hook chaining Jump to heading

You can register multiple hooks; they form a chain. Hooks run in LIFO (last registered, first called) order, and each hook can call nextResolve() / nextLoad() to pass control to the previous hook in the chain:

import { registerHooks } from "node:module";

// Hook 1: registered first, runs second
const hook1 = registerHooks({
  load(url, context, nextLoad) {
    const result = nextLoad(url, context);
    if (url.includes("target.js")) {
      return {
        source: 'export default "from hook1"',
        format: "module",
        shortCircuit: true,
      };
    }
    return result;
  },
});

// Hook 2: registered second, runs first
const hook2 = registerHooks({
  load(url, context, nextLoad) {
    const result = nextLoad(url, context); // Calls hook1
    if (url.includes("target.js")) {
      return {
        source: 'export default "from hook2"',
        format: "module",
        shortCircuit: true,
      };
    }
    return result;
  },
});

// Result comes from hook2 since it runs first (LIFO)

CommonJS Jump to heading

Hooks also intercept require():

main.cjs
const { registerHooks } = require("module");

const hooks = registerHooks({
  resolve(specifier, context, nextResolve) {
    if (specifier === "virtual-module") {
      return { url: "file:///virtual.js", shortCircuit: true };
    }
    return nextResolve(specifier, context);
  },
  load(url, context, nextLoad) {
    if (url === "file:///virtual.js") {
      return {
        source: "module.exports = { value: 42 }",
        format: "commonjs",
        shortCircuit: true,
      };
    }
    return nextLoad(url, context);
  },
});

const mod = require("virtual-module");
console.log(mod.value); // 42

hooks.deregister();

Last updated on

Did you find what you needed?

Privacy policy