ES Modules Logo ESModules.com

JavaScript Runtimes & ESM

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 (v22+ / v24 LTS)

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 2.x

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:

  • Source phase imports for WebAssembly
  • deno audit for dependency security scanning
  • Built-in permission model (--allow-read, --allow-net)
  • Native TypeScript execution without compilation

Bun 1.3+

Bun 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

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:

  • Limited Node.js API subset (no fs, child_process, etc.)
  • Web-standard APIs (fetch, Request, Response, crypto)
  • Short execution time limits (typically 10-50ms CPU time)
  • No file system access - all state is external

Runtime Comparison

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

Frequently Asked Questions