| name | esm-module-patterns |
| description | Modern ESM import/export patterns. Use when writing or reviewing module structure. |
ESM Module Patterns Skill
This skill covers modern ECMAScript Module (ESM) patterns for TypeScript projects.
When to Use
Use this skill when:
- Setting up new TypeScript projects
- Converting CommonJS to ESM
- Designing module structure
- Reviewing import/export patterns
Core Principle
ESM ONLY - No CommonJS (require/module.exports). All projects use native ES Modules.
Package.json Configuration
{
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist"]
}
Import Patterns
Named Imports (Preferred)
// ✅ Named imports - clear and tree-shakeable
import { formatDate, parseDate } from './utils/date.js';
import { UserService, type User } from './services/user.js';
// ✅ Type-only imports
import type { Config } from './config.js';
Default Imports
// ✅ Default import for single main export
import express from 'express';
import React from 'react';
// ✅ Default with named
import fs, { promises as fsp } from 'node:fs';
Namespace Imports
// ✅ Namespace import when many exports needed
import * as utils from './utils/index.js';
utils.formatDate(date);
utils.parseDate(str);
Dynamic Imports
// ✅ Dynamic import for code splitting
const module = await import('./heavy-module.js');
// ✅ Conditional loading
if (process.env.NODE_ENV === 'development') {
const devTools = await import('./dev-tools.js');
devTools.enable();
}
Export Patterns
Named Exports (Preferred)
// ✅ Named exports - explicit and tree-shakeable
export function formatDate(date: Date): string {
return date.toISOString();
}
export interface User {
id: string;
name: string;
}
export const DEFAULT_TIMEOUT = 5000;
Default Exports
// ✅ Default export for main module entry
export default class ApiClient {
// ...
}
// ✅ Default export for React components
export default function Button({ children }: ButtonProps) {
return <button>{children}</button>;
}
Re-exports
// ✅ Re-export from barrel file (index.ts)
export { formatDate, parseDate } from './date.js';
export { User, UserService } from './user.js';
export type { Config } from './config.js';
// ✅ Re-export with rename
export { internalFunction as publicFunction } from './internal.js';
// ✅ Re-export all
export * from './utils.js';
export * as helpers from './helpers.js';
File Extensions
Always use .js extension in imports, even for TypeScript files:
// ✅ Correct - .js extension
import { helper } from './utils/helper.js';
import { User } from '../models/user.js';
// ❌ Wrong - no extension
import { helper } from './utils/helper';
// ❌ Wrong - .ts extension
import { helper } from './utils/helper.ts';
Why .js? TypeScript compiles .ts to .js, and Node.js ESM requires extensions.
tsconfig.json for ESM
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"isolatedModules": true
}
}
Directory Structure
src/
├── index.ts # Main entry point
├── types/
│ └── index.ts # Type exports
├── utils/
│ ├── index.ts # Barrel file
│ ├── date.ts
│ └── string.ts
├── services/
│ ├── index.ts # Barrel file
│ ├── api.ts
│ └── auth.ts
└── models/
├── index.ts # Barrel file
└── user.ts
Barrel Files (index.ts)
Use barrel files to simplify imports:
// src/utils/index.ts
export { formatDate, parseDate } from './date.js';
export { capitalize, truncate } from './string.js';
export { debounce, throttle } from './async.js';
// Consumer code
import { formatDate, capitalize, debounce } from './utils/index.js';
// or
import { formatDate, capitalize, debounce } from './utils/index.js';
Node.js Built-in Modules
Use node: prefix for Node.js built-in modules:
// ✅ With node: prefix
import fs from 'node:fs';
import path from 'node:path';
import { createServer } from 'node:http';
// ❌ Without prefix (works but not recommended)
import fs from 'fs';
CommonJS Interop
When importing CommonJS modules:
// ✅ Default import for CommonJS modules
import lodash from 'lodash';
// ✅ Named imports if supported
import { debounce } from 'lodash';
// ⚠️ May need default for some CJS modules
import pkg from 'some-cjs-package';
const { namedExport } = pkg;
Anti-Patterns
Mixed Module Systems
// ❌ Never use require() in ESM
const fs = require('fs');
// ❌ Never use module.exports
module.exports = { foo };
// ❌ Never use __dirname/__filename directly
console.log(__dirname);
ESM Replacements for CommonJS Globals
// ✅ ESM way to get __dirname and __filename
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Circular Dependencies
// ❌ Avoid circular imports
// a.ts
import { b } from './b.js';
export const a = b + 1;
// b.ts
import { a } from './a.js'; // Circular!
export const b = a + 1;
// ✅ Break cycle with third module or restructure
// shared.ts
export const base = 1;
// a.ts
import { base } from './shared.js';
export const a = base + 1;
// b.ts
import { base } from './shared.js';
export const b = base + 2;
Type-Only Imports
Use type keyword for imports used only in type positions:
// ✅ Type-only import - removed at compile time
import type { User, Config } from './types.js';
// ✅ Inline type import
import { createUser, type User } from './user.js';
// ✅ Type-only re-export
export type { User, Config } from './types.js';
Best Practices Summary
- Always use
"type": "module"in package.json - Use
.jsextension in all imports - Prefer named exports over default exports
- Use
node:prefix for Node.js built-ins - Use type-only imports for types
- Create barrel files for clean API surface
- Avoid circular dependencies
- Never mix ESM and CommonJS
Code Review Checklist
- package.json has
"type": "module" - All imports use
.jsextension - No
require()ormodule.exports - Node.js built-ins use
node:prefix - Type-only imports use
typekeyword - No circular dependencies
- Barrel files export public API only