ES Modules (import)
ES Modules are JavaScript's standard module system - static import/export, dynamic import(), and top-level await give Node 24 services explicit dependency graphs and async initialization.
Search across all documentation pages
import)ES Modules are JavaScript's standard module system - static import/export, dynamic import(), and top-level await give Node 24 services explicit dependency graphs and async initialization.
import { readFile } from 'node:fs/promises';
import type { Server } from 'node:http';
export async function loadConfig(path: string): Promise<Record<string, string>> {
const raw = await readFile(path, 'utf8');
return JSON.parse(raw) as Record<string, string>;
}const mod = await import('./feature.js');When to reach for this:
"type": "module")// config.ts - top-level await loads before server imports this module
import { readFile } from 'node:fs/promises';
export const config = JSON.parse(
await readFile(new URL('./config.json', import.meta.url), 'utf8'),
) as { port: number; host: string };// server.ts
import { createServer } from 'node:http';
import { config } from './config.js';
const server = createServer((req, res) => {
res.end(`listening config port ${config.port}`);
});
server.listen(config.port, config.host);// plugins/loader.ts
const pluginName = process.env.PLUGIN ?? 'default';
const plugin = await import(`./${pluginName}.js`);
await plugin.register();What this demonstrates:
await blocks importers until config is ready - no manual async main wrapperimport.meta.url resolves relative paths in ESM without __dirnameimport() enables plugin selection from environment variableslet/const exports).Promise<Module> - works in CJS via async wrapper and in ESM anywhere.with { type: 'json' }.| Form | When |
|---|---|
import { x } from './a.js' | Static named imports |
import type { T } from './a.js' | Type-only (erased) |
import * as ns from './a.js' | Namespace object |
await import('./a.js') | Runtime conditional loading |
// tsconfig.json for Node 24 ESM
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"verbatimModuleSyntax": true
}
}Use NodeNext so TypeScript enforces .js extensions matching Node resolution.
.js extension in import paths - ERR_MODULE_NOT_FOUND at runtime after tsc. Fix: moduleResolution: NodeNext and import './file.js'.import fs from 'node:fs' fails without default export. Fix: import * as fs from 'node:fs' or named imports.| Alternative | Use When | Don't Use When |
|---|---|---|
CommonJS require | Legacy .cjs only | Greenfield services |
createRequire | One-off CJS dep from ESM | Whole app could be ESM |
| Bundler (esbuild) | Single-file deploy artifact | Server runs TS directly with tsx |
import.meta.resolve | Resolve paths spec-compliant | Simple relative imports suffice |
Yes for Node ESM resolution of relative files - use .js in import specifiers even when source is .ts.
The absolute file:// URL of the current module - use with new URL('./x', import.meta.url).
Yes in ESM test files run with node --test or Vitest ESM mode.
export { foo } from './foo.js' and export * from './bar.js' build public API surfaces.
Static imports are evaluated before the module body runs, in dependency order.
Node supports JSON modules via import attributes in recent versions - or use readFile + parse for clarity.
Node 24+ exposes whether the module is the entry point - useful for dual CLI/library files.
It does not exist. Use fileURLToPath(import.meta.url) and path.dirname.
Increasingly via SWC and "type": "module" projects - verify decorator/metadata compatibility in your stack.
No - it always returns a Promise. Use static import for sync module graph needs.
If exporter changes export let count = 1 to 2, importers see 2 when they read count - unlike deep copy.
import { readFile } from 'node:fs/promises' - no extension, node: prefix recommended.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 16, 2026