Compatibility
Node.js compatibility layers in Deno and Bun for running existing npm packages and Node APIs
You are an expert in Node.js compatibility layers for Deno and Bun, enabling gradual adoption of modern runtimes while preserving access to the npm ecosystem. ## Key Points - Implements Node built-in modules directly (`fs`, `path`, `http`, `crypto`, `child_process`, etc.) - Supports both `require()` and `import` - Reads `package.json` and installs to `node_modules` - Supports many N-API native addons - Aims for full Node API coverage; gaps are tracked and shrinking with each release - Supports Node built-ins via `node:` prefix (`import fs from "node:fs"`) - Runs npm packages via `npm:` specifier or `nodeModulesDir` mode - Does not support `require()` natively (use `createRequire` from `node:module`) - Most popular npm packages work; some with native addons do not - Permissions model still applies — even `node:fs` calls need `--allow-read` - **Always use the `node:` prefix** for Node built-in imports — it works across all three runtimes (Node 16+, Deno, Bun) and makes the intent explicit. - **Prefer Web Standard APIs** (`fetch`, `Request`, `Response`, `URL`, `crypto.subtle`, `ReadableStream`) over Node-specific ones when building new code. They are portable by design. ## Quick Example ```bash deno run --allow-net --allow-read --allow-env server.ts ``` ```bash bun run server.ts # Just works ```
skilldb get deno-bun-skills/CompatibilityFull skill: 289 linesNode.js Compatibility Layers — Modern JS Runtimes
You are an expert in Node.js compatibility layers for Deno and Bun, enabling gradual adoption of modern runtimes while preserving access to the npm ecosystem.
Overview
Both Deno and Bun implement substantial portions of the Node.js API surface to run existing npm packages and Node-style code. Bun targets near-complete API compatibility as a drop-in replacement. Deno takes a more deliberate approach, requiring the node: import prefix and providing a nodeModulesDir option for npm package resolution. Understanding what works, what partially works, and what breaks is critical for a smooth migration.
Core Concepts
Compatibility Approach by Runtime
Bun:
- Implements Node built-in modules directly (
fs,path,http,crypto,child_process, etc.) - Supports both
require()andimport - Reads
package.jsonand installs tonode_modules - Supports many N-API native addons
- Aims for full Node API coverage; gaps are tracked and shrinking with each release
Deno:
- Supports Node built-ins via
node:prefix (import fs from "node:fs") - Runs npm packages via
npm:specifier ornodeModulesDirmode - Does not support
require()natively (usecreateRequirefromnode:module) - Most popular npm packages work; some with native addons do not
- Permissions model still applies — even
node:fscalls need--allow-read
Module Resolution Differences
| Feature | Node.js | Bun | Deno |
|---|---|---|---|
require("fs") | Yes | Yes | No (use createRequire) |
import "fs" | Yes | Yes | No (use "node:fs") |
import "node:fs" | Yes (16+) | Yes | Yes |
import "npm:pkg" | No | No | Yes |
Bare specifiers (import "lodash") | Via node_modules | Via node_modules | Via import map or nodeModulesDir |
.ts file extensions in imports | No (needs tsc) | Yes | Yes |
Implementation Patterns
Using Node Built-ins in Deno
// Deno requires the node: prefix for all Node built-in modules
import { readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { createServer } from "node:http";
import { Buffer } from "node:buffer";
import { EventEmitter } from "node:events";
import { createHash } from "node:crypto";
import { Readable, Transform } from "node:stream";
const content = readFileSync("./data.txt", "utf-8");
const hash = createHash("sha256").update(content).digest("hex");
const fullPath = path.resolve(".", "output", "result.txt");
writeFileSync(fullPath, hash);
Using npm Packages in Deno
// Option 1: npm: specifier (inline, no install step)
import express from "npm:express@4";
import { z } from "npm:zod@3";
import pg from "npm:pg@8";
// Option 2: Import map in deno.json
// {
// "imports": {
// "express": "npm:express@4",
// "zod": "npm:zod@3"
// }
// }
import express from "express";
import { z } from "zod";
// Option 3: nodeModulesDir mode (closest to Node workflow)
// deno.json: { "nodeModulesDir": "auto" }
// Then: deno install
// Then use bare specifiers as in Node
Using require() in Deno
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
// Now you can use require for CJS packages
const _ = require("lodash");
const config = require("./config.json");
Polyfilling Node Globals in Deno
// process — available automatically in Deno when running npm packages,
// or import explicitly:
import process from "node:process";
console.log(process.env.HOME);
console.log(process.argv);
console.log(process.cwd());
// Buffer — available automatically in npm packages,
// or import explicitly:
import { Buffer } from "node:buffer";
const buf = Buffer.from("hello", "utf-8");
// __dirname and __filename equivalents
const __filename = import.meta.filename; // Deno 1.40+
const __dirname = import.meta.dirname; // Deno 1.40+
// For older Deno versions:
import { fileURLToPath } from "node:url";
import path from "node:path";
const __filename_compat = fileURLToPath(import.meta.url);
const __dirname_compat = path.dirname(__filename_compat);
Running Express on Deno
import express from "npm:express@4";
import cors from "npm:cors@2";
const app = express();
app.use(cors());
app.use(express.json());
app.get("/api/health", (_req, res) => {
res.json({ status: "ok", runtime: "deno" });
});
app.listen(3000, () => {
console.log("Express running on Deno at :3000");
});
deno run --allow-net --allow-read --allow-env server.ts
Running Express on Bun (No Changes Needed)
// Exact same code as Node — no modifications required
import express from "express";
import cors from "cors";
const app = express();
app.use(cors());
app.use(express.json());
app.get("/api/health", (_req, res) => {
res.json({ status: "ok", runtime: "bun" });
});
app.listen(3000, () => {
console.log("Express running on Bun at :3000");
});
bun run server.ts # Just works
Cross-Runtime Portable Code
Write code that works on Node, Deno, and Bun:
// Detect runtime
function getRuntime(): "deno" | "bun" | "node" {
if ("Deno" in globalThis) return "deno";
if ("Bun" in globalThis) return "bun";
return "node";
}
// Use Web Standard APIs for maximum portability
async function fetchData(url: string) {
// fetch() works on all three runtimes
const resp = await fetch(url);
return resp.json();
}
// Use node: prefix — works on Node 16+, Deno, and Bun
import { readFile } from "node:fs/promises";
import path from "node:path";
import { createHash } from "node:crypto";
Handling Conditional Imports
// Dynamic imports for runtime-specific code
async function getKV() {
const runtime = getRuntime();
if (runtime === "deno") {
return await Deno.openKv();
} else if (runtime === "bun") {
const { Database } = await import("bun:sqlite");
return new Database("kv.db");
} else {
const { Level } = await import("level");
return new Level("./kv-store");
}
}
Node API Compatibility Matrix
// WELL SUPPORTED on both Deno and Bun:
import fs from "node:fs"; // filesystem
import fsp from "node:fs/promises"; // async filesystem
import path from "node:path"; // path utilities
import { URL } from "node:url"; // URL parsing
import crypto from "node:crypto"; // cryptography
import { Buffer } from "node:buffer"; // binary data
import { EventEmitter } from "node:events"; // events
import { Readable, Writable } from "node:stream"; // streams
import os from "node:os"; // OS info
import util from "node:util"; // utilities
import querystring from "node:querystring"; // query strings
import assert from "node:assert"; // assertions
// PARTIALLY SUPPORTED (some methods may be missing):
import http from "node:http"; // HTTP server/client
import https from "node:https"; // HTTPS
import net from "node:net"; // TCP
import tls from "node:tls"; // TLS
import child_process from "node:child_process"; // subprocesses
import worker_threads from "node:worker_threads"; // workers
import zlib from "node:zlib"; // compression
// LIMITED or UNSUPPORTED:
// node:vm — partial in Bun, limited in Deno
// node:inspector — not available
// node:trace_events — not available
// node:perf_hooks — partial
// node:diagnostics_channel — partial in Bun
// node:cluster — not available in Deno, partial in Bun
Core Philosophy
The Node.js compatibility layers in Deno and Bun exist to solve a pragmatic problem: the npm ecosystem contains millions of packages, and requiring developers to rewrite everything from scratch would make modern runtimes unusable. Compatibility is a migration bridge, not a design goal. The intent is to let you run existing code today while gradually adopting the native APIs and module system of your chosen runtime.
Bun and Deno take fundamentally different approaches to compatibility, reflecting their different philosophies. Bun aims to be a transparent drop-in replacement where package.json, node_modules, require(), and bare specifiers all work exactly as they do in Node. Deno takes a more explicit approach, requiring the node: prefix for built-in modules and npm: specifiers for npm packages. Both strategies have merit: Bun minimizes migration effort, while Deno makes the compatibility boundary visible in the source code.
The node: prefix is the one convention that works across all three runtimes (Node 16+, Deno, Bun) and should be your default for writing portable code. When combined with Web Standard APIs (fetch, Request, Response, URL, crypto.subtle), you can write code that genuinely runs on any modern JavaScript runtime without modification. This portability is not theoretical; it is the foundation of cross-runtime frameworks like Hono and libraries that target the WinterCG standard.
Anti-Patterns
-
Importing Node built-ins without the
node:prefix. Whileimport fs from "fs"works in Node and Bun, it fails in Deno. Always writeimport fs from "node:fs"for cross-runtime compatibility. -
Relying on native C++ V8 addons. Packages that use raw V8 C++ APIs (not N-API) will not work on Bun or Deno. Check whether your dependency uses N-API or has a pure JavaScript/Wasm alternative before committing to a migration.
-
Assuming
process.envmutation behavior is identical across runtimes. In Node, settingprocess.env.FOO = "bar"affects child processes. In Deno, environment modification requiresDeno.env.set()with permissions. In Bun, assignment works but propagation may differ. Test environment handling explicitly. -
Shipping polyfills for APIs that are already built in. Some npm packages bundle polyfills for
fetch,URL,Buffer, orTextEncoderthat conflict with the runtime's native implementations. These cause type mismatches and subtle bugs. Remove unnecessary polyfill packages when migrating. -
Mixing Node streams and Web streams without conversion. Piping a Node
Readableto a WebWritableStream(or vice versa) fails silently or throws. UseReadable.toWeb()andWritable.toWeb()adapters to bridge the two stream APIs explicitly.
Best Practices
- Always use the
node:prefix for Node built-in imports — it works across all three runtimes (Node 16+, Deno, Bun) and makes the intent explicit. - Prefer Web Standard APIs (
fetch,Request,Response,URL,crypto.subtle,ReadableStream) over Node-specific ones when building new code. They are portable by design. - Test on the target runtime early — install Bun or Deno in CI alongside Node and run tests on both before fully committing.
- Use
nodeModulesDir: "auto"in Deno during migration to avoid rewriting all imports at once. - Check the compatibility tables for your runtime version before assuming an API works. Both Deno and Bun publish detailed compatibility docs.
- Isolate runtime-specific code behind abstraction layers so the bulk of your application remains portable.
Common Pitfalls
- Missing
node:prefix in Deno —import fs from "fs"fails in Deno. Always write"node:fs". - Native C++ addons — packages like
better-sqlite3,canvas, ornode-sassthat use raw V8 C++ API will not work. N-API based addons generally work in Bun. Look for Wasm or pure JS alternatives. - Subtle behavior differences in
node:http— both runtimes implementhttp.createServerbut edge cases (keep-alive behavior, timeout handling, trailer headers) may differ from Node. Test HTTP-level behavior thoroughly. process.envmutation — in Node, modifyingprocess.envaffects child processes. In Deno,Deno.env.set()is required (and needs--allow-env). In Bun,process.envassignment works but may not propagate identically.package.jsonexportsfield — both runtimes support it, but resolution edge cases (conditional exports, self-referencing) may differ from Node. If an import fails, check the package'sexportsmap.- Polyfill conflicts — some npm packages ship their own polyfills for
fetch,URL, orBufferthat collide with built-in implementations. This causes type mismatches or runtime errors. Remove unnecessary polyfill packages. - Stream compatibility — Node streams and Web streams are different APIs. Mixing them (e.g., piping a Node
Readableto a WebWritableStream) requires conversion. UseReadable.toWeb()andWritable.toWeb()adapters. - TypeScript configuration — Deno ignores
tsconfig.jsonby default (usesdeno.jsoncompilerOptions). Bun readstsconfig.json. Keep configuration in sync during migration.
Install this skill directly: skilldb add deno-bun-skills
Related Skills
Bun Basics
Bun runtime fundamentals including speed optimizations, built-in APIs, and package management
Bun Bundler
Using Bun as a bundler for frontend assets and as a fast test runner
Deno Basics
Deno runtime fundamentals including permissions, module system, and built-in tooling
Deno Deploy
Deno Deploy edge functions for globally distributed serverless applications
Elysia Bun
Elysia web framework on Bun for type-safe, high-performance HTTP APIs
Fresh Framework
Fresh full-stack web framework for Deno with islands architecture and zero client JS by default