Customization hooks let you intervene at the two points the resolver exposes: resolve, which turns a specifier into a URL, and load, which turns a URL into source text and a format. This is the machinery behind TypeScript runners such as tsx 12,162 , coverage instrumenters and test doubles that replace a module without touching its importers.
Node 26 has picked a winner between the two registration APIs. module.registerHooks() (23.5.0, still a release candidate) installs synchronous hooks that run on the main thread. The older module.register() runs asynchronous hooks on a separate loader thread; it is deprecated as of Node 25.9.0 and warns at runtime (DEP0205) in Node 26. Write new hooks with registerHooks().
import { registerHooks } from 'node:module';
registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier.startsWith('config:')) {
const value = process.env[specifier.slice(7)] ?? 'unset';
const url = `data:text/javascript,export default ${JSON.stringify(value)}`;
return { url, shortCircuit: true };
}
return nextResolve(specifier, context);
},
load(url, context, nextLoad) {
const result = nextLoad(url, context);
if (!url.endsWith('.timed.js')) return result;
const body = result.source.toString();
const stamp = "console.time('module');\n";
return { ...result, source: stamp + body + "console.timeEnd('module');\n" };
},
});app.js then does two ordinary imports -- const { default: port } = await import('config:PORT') and await import('./report.timed.js') -- and logs the port and the row count. Hooks must be installed before the modules they affect are loaded, so preload them with --import. Running PORT=8080 node --import ./env-hooks.js app.js gives:
module: 3.221ms port=8080 rows=50000
Three rules keep hooks maintainable. Always call nextResolve or nextLoad for anything you do not handle: hooks form a chain and yours is not the only one. Return shortCircuit: true only when you deliberately stop that chain, as the config: specifier does. And keep the work cheap -- a synchronous hook runs for every module in the graph, so a slow regular expression lands straight in your startup time.