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. UseregisterHooks()for both CommonJS and ESM customization.
Basic example Jump to heading
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).
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);
},
});
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():
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();