Skip to content
Back to skills

Esm Module Patterns

ASecurity

Modern ESM import/export patterns. Use when writing or reviewing module structure.

  • 666 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added February 8, 2026
developmenttypescriptreactnodeexpressapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add majiayu000/claude-skill-registry --skill esm-module-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Esm Module Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Esm Module Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/majiayu000-esm-module-patterns/badge)](https://www.skillsdirectory.com/skills/majiayu000-esm-module-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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

```json
{
  "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)

```typescript
// ✅ 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

```typescript
// ✅ 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

```typescript
// ✅ Namespace import when many exports needed
import * as utils from './utils/index.js';

utils.formatDate(date);
utils.parseDate(str);
```

### Dynamic Imports

```typescript
// ✅ 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)

```typescript
// ✅ 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

```typescript
// ✅ 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

```typescript
// ✅ 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:

```typescript
// ✅ 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

```json
{
  "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:

```typescript
// src/utils/index.ts
export { formatDate, parseDate } from './date.js';
export { capitalize, truncate } from './string.js';
export { debounce, throttle } from './async.js';
```

```typescript
// 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:

```typescript
// ✅ 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:

```typescript
// ✅ 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

```typescript
// ❌ 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

```typescript
// ✅ 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

```typescript
// ❌ 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:

```typescript
// ✅ 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

1. **Always use `"type": "module"` in package.json**
2. **Use `.js` extension in all imports**
3. **Prefer named exports over default exports**
4. **Use `node:` prefix for Node.js built-ins**
5. **Use type-only imports for types**
6. **Create barrel files for clean API surface**
7. **Avoid circular dependencies**
8. **Never mix ESM and CommonJS**

## Code Review Checklist

- [ ] package.json has `"type": "module"`
- [ ] All imports use `.js` extension
- [ ] No `require()` or `module.exports`
- [ ] Node.js built-ins use `node:` prefix
- [ ] Type-only imports use `type` keyword
- [ ] No circular dependencies
- [ ] Barrel files export public API only

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.