ES Modules across Node.js, Deno, Bun, and edge runtimes. Each runtime has unique ESM features and configuration. Learn the differences and best practices for each.
Node.js has evolved from experimental ESM support to making it fully production-ready. Key milestones:
Node 16 (2021)
ESM support stabilized. No longer experimental.
Node 22 (2024)
require(esm) behind flag. import.meta.dirname/filename.
Node 23 (2024)
require(esm) enabled by default. Import attributes stable.
Node 24 LTS (2025)
Built-in TypeScript type stripping. Fully mature ESM.
// package.json - enable ESM for all .js files
{ "type": "module" }
// Node.js-specific import.meta properties
console.log(import.meta.url); // file:///path/to/file.js
console.log(import.meta.dirname); // /path/to (Node 21.2+)
console.log(import.meta.filename); // /path/to/file.js (Node 21.2+)
console.log(import.meta.resolve('./other.js')); // Resolve specifier
// require(esm) - CJS can now load ESM (Node 23+)
// In a .cjs file:
const { add } = require('./math.mjs'); // Works!
Deno was built ESM-first from day one. Version 2.x adds full Node.js and npm backwards compatibility while keeping the ESM-native experience.
// Deno uses ESM natively - no configuration needed
import { serve } from "https://deno.land/std/http/server.ts";
// npm packages via npm: specifier
import express from "npm:express@5";
// JSR registry (ESM-native package registry)
import { parse } from "jsr:@std/csv";
// deno.json - project configuration with import maps
{
"imports": {
"lodash": "npm:[email protected]",
"@std/": "jsr:@std/"
},
"tasks": {
"dev": "deno run --watch main.ts",
"test": "deno test"
}
}
// Deno-specific features
console.log(import.meta.url); // file:///path/to/file.ts
console.log(import.meta.main); // true if entry point
console.log(import.meta.dirname); // Available in Deno 2.x
Deno 2.6 Highlights:
deno audit for dependency security scanningBun handles ESM and CommonJS interchangeably in any file without requiring extensions or configuration. Its Zig-based runtime focuses on speed.
// Bun treats all files as modules by default
// ESM and CJS work interchangeably
import express from 'express'; // ESM import of CJS package
const fs = require('node:fs'); // CJS require in ESM file
// Both work in the same file!
// Bun's built-in bundler with ESM output
// bunfig.toml
[build]
entrypoints = ["./src/index.ts"]
outdir = "./dist"
format = "esm"
// Bun 1.3: ESM bytecode compilation
// bun build --bytecode --format=esm ./src/index.ts
// Built-in TypeScript and JSX support
// No tsconfig needed - just works
import { Component } from './MyComponent.tsx';
Bun's Approach:
Bun doesn't distinguish between ESM and CJS at the file level. You can mix import and require() freely. This eliminates the module format friction entirely, though it may mask compatibility issues when code runs in other runtimes.
Edge runtimes run JavaScript at CDN edge locations for low-latency responses. They all use ES Module format as their standard.
// Cloudflare Workers (ESM format)
export default {
async fetch(request, env) {
const url = new URL(request.url);
return new Response('Hello from the edge!', {
headers: { 'content-type': 'text/plain' }
});
}
};
// Vercel Edge Functions
export const config = { runtime: 'edge' };
export default async function handler(request) {
return new Response('Hello from Vercel Edge');
}
// Netlify Edge Functions
export default async (request, context) => {
return new Response('Hello from Netlify Edge');
};
Key Differences from Node.js:
| Feature | Node.js 24 | Deno 2.x | Bun 1.3 |
|---|---|---|---|
| ESM Default | With "type": "module" | Native | Native |
| CJS Interop | require(esm) default | npm: compat | Seamless |
| TypeScript | Type stripping | Native | Native |
| Import Attributes | Stable | Stable | Stable |
| Package Manager | npm / pnpm / yarn | deno add (npm + JSR) | bun install |
| Permission Model | Experimental | Built-in | None |
| Built-in Bundler | No | Basic | Yes |
| Built-in Test Runner | node --test | deno test | bun test |
Deno and Bun were built ESM-first and have the most seamless experience. Node.js 22+ has closed the gap significantly with stable ESM support and require(esm) interop. All three runtimes now provide excellent ESM support for production use.
Yes! Starting with Node.js 22, CommonJS code can require() ES Modules (enabled by default in Node 23+). This only works for synchronous ESM modules - modules using top-level await still require dynamic import().
Yes, Deno 2.x has full npm compatibility. Use 'npm:' specifiers to import npm packages directly, or add them to deno.json. Deno also supports the JSR registry for ESM-native packages.
Edge runtimes like Cloudflare Workers and Vercel Edge Functions run JavaScript at CDN edge locations. They use ES Module format as their standard module system, with a subset of Node.js APIs available.