ES Modules Logo ESModules.com

In Practice

Learn how ES Modules work in real-world environments. Master module loading in browsers, configure Node.js for ESM, and understand the practical differences between platforms.

ES Modules in the Browser

The <script type="module"> Tag

To use ES Modules in browsers, add the type="module" attribute to your script tag.

<!-- Basic module script -->
<script type="module" src="./main.js"></script>

<!-- Inline module code -->
<script type="module">
  import { greet } from './utils.js';
  greet('World');
</script>

Key Behaviors:

  • Automatically runs in strict mode
  • Deferred by default (like defer attribute)
  • Own scope (variables don't leak to global)
  • Can use await at top level

Module Specifiers

Different types of module paths you can use:

// Relative paths (must include extension)
import { utils } from './utils.js';
import { helper } from '../helpers/helper.js';

// Absolute paths
import { api } from '/src/api/client.js';

// URLs (full path)
import { library } from 'https://cdn.example.com/lib.js';

CORS Restrictions

ES Modules are subject to CORS policy. You cannot load modules from file:// protocol.

❌ Won't Work:

Opening index.html directly in browser (file:// protocol)

✅ Use a Development Server:

# Using Python
python -m http.server 8000

# Using Node.js
npx serve .

# Using VS Code
# Install "Live Server" extension

Module vs Classic Script

Feature Module Script Classic Script
Strict Mode Always Optional
Defer Loading Default Optional
Scope Module scope Global scope
Top-level await Yes No
CORS Required Not required

ES Modules in Node.js

Enabling ESM

Node.js supports ES Modules since version 12. Two methods to enable:

Method 1: Use .mjs Extension

// utils.mjs
export function add(a, b) {
  return a + b;
}

// app.mjs
import { add } from './utils.mjs';
console.log(add(2, 3));

Method 2: Set "type": "module" in package.json

{
  "name": "my-app",
  "version": "1.0.0",
  "type": "module"
}

All .js files will be treated as ES Modules

Replacing __dirname and __filename

These CommonJS globals aren't available in ESM. Use import.meta.url instead:

import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

console.log('Current file:', __filename);
console.log('Current directory:', __dirname);

Importing Built-in Modules

Node.js built-in modules can be imported with or without node: prefix:

// Both work, but node: prefix is recommended
import fs from 'node:fs';
import path from 'node:path';
import { readFile } from 'node:fs/promises';

// Also valid (without prefix)
import fs from 'fs';
import path from 'path';

JSON Imports

Import JSON files with import assertions:

// Node.js 17.1+
import config from './config.json' assert { type: 'json' };

// Alternative: use fs
import { readFile } from 'node:fs/promises';
const config = JSON.parse(
  await readFile('./config.json', 'utf-8')
);

Conditional Exports

Define different entry points for ESM and CommonJS in package.json:

{
  "name": "my-library",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "import": "./dist/utils.js",
      "require": "./dist/utils.cjs"
    }
  }
}