Modules Basics
8 examples to get you started with Modules - 6 basic and 2 intermediate.
Prerequisites
- Node.js 24.18.0 with
"type": "module"inpackage.jsonfor ESM examples. - Understand Node.js Basics project layout.
Basic Examples
1. ESM Export and Import
Named exports are statically analyzable - the preferred default in 2026.
// user.ts
export interface User { id: string; name: string }
export function formatUser(u: User): string {
return `${u.name} (${u.id})`;
}// main.ts
import { formatUser, type User } from './user.js';
const user: User = { id: '1', name: 'Ada' };
console.log(formatUser(user));- Use
.jsextensions in import specifiers when compiling TypeScript to ESM. import typeerases at compile time - no runtime cost.- ESM enables tree shaking in bundlers and explicit public APIs.
Related: ES Modules (import) - dynamic import and TLA
2. Default Export (Sparingly)
One default export per module - use for single-purpose entry points.
// logger.ts
export default function createLogger(prefix: string) {
return (msg: string) => console.log(`[${prefix}]`, msg);
}import createLogger from './logger.js';
const log = createLogger('api');
log('started');- Prefer named exports for libraries - better refactoring and autocomplete.
- Default exports complicate re-exports in barrel files.
- Framework configs often use default exports by convention.
3. CommonJS require (Legacy)
Brownfield code still uses require - know it for maintenance.
// legacy.cjs
const path = require('node:path');
module.exports = {
basename: (p) => path.basename(p),
};const { basename } = require('./legacy.cjs');.cjsextension forces CommonJS even when"type": "module".requireis synchronous - no top-level await in CJS modules.- Migrate leaf files to ESM before route handlers.
Related: CommonJS (require) - module.exports patterns
4. package.json "type": "module"
Sets ESM as default for .js files in the package.
{
"name": "billing-api",
"type": "module",
"exports": {
".": "./dist/index.js"
}
}- Without
"type": "module",.jsfiles are treated as CommonJS. "type": "commonjs"is explicit legacy mode.- Published libraries document
"exports"for public entry points.
Related: package.json type & exports - conditional exports
5. node: Built-in Prefix
Clarify built-in imports and avoid user package shadowing.
import { readFile } from 'node:fs/promises';
import { createServer } from 'node:http';node:prefix is optional but recommended in application code.- Prevents a local
fspackage from hijacking imports. - Works in both ESM and CJS (
require('node:fs')).
6. Dynamic import()
Load modules conditionally or at runtime - returns a Promise.
async function loadPlugin(name: string) {
const mod = await import(`./plugins/${name}.js`);
return mod.default();
}
await loadPlugin('metrics');- Dynamic specifiers must stay within analyzable bounds for bundlers.
- Useful for optional features and code splitting.
- Cannot replace static imports for tree-shaking critical paths.
Related: ES Modules (import) - top-level await with dynamic import
Intermediate Examples
7. Barrel File (Public API)
Re-export a curated surface from index.ts.
// src/services/index.ts
export { UserService } from './user-service.js';
export type { User } from './user-service.js';import { UserService, type User } from './services/index.js';- Barrel files simplify imports but can slow IDE and bundlers if huge.
- Export only public types - hide internal modules.
- Avoid circular barrels - split domains instead.
Related: Module Resolution Algorithm - how index resolves
8. CJS ↔ ESM Interop in One Repo
Mixed modules during migration - explicit extensions and createRequire.
// bridge.mjs
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const legacy = require('./legacy-config.cjs');
console.log(legacy.apiUrl);createRequireloads CJS from ESM context during brownfield migration.- Plan an ADR sunset date for dual-module packages.
- Test both
importandrequirepaths in CI during transition.
Related: CJS ↔ ESM Interop - dual packages and .mjs/.cjs
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.