TypeScript Runtimes
Overview
The JavaScript/TypeScript server-side runtime ecosystem has evolved from a single dominant platform (Node.js) into a competitive landscape with three major runtimes: Node.js, Deno, and Bun. Each offers native or near-native TypeScript support, but they differ significantly in their security models, standard libraries, tooling philosophies, and deployment targets. This skill provides a high-level comparison to help choose the right runtime for a given project.
All three runtimes execute JavaScript via high-performance engines (V8 for Node.js and Deno, JavaScriptCore for Bun) and support modern ECMAScript features, async/await, and the event-driven programming model that defines server-side JavaScript.
Runtime Comparison
| Feature | Node.js | Deno | Bun |
|---|---|---|---|
| Engine | V8 (C++) | V8 (Rust) | JavaScriptCore (Zig/C++) |
| TypeScript support | Via transpiler (tsx, ts-node, swc, esbuild); experimental --experimental-strip-types in Node 22+ | Native -- runs .ts files with zero config | Native -- runs .ts files with zero config |
| Package manager | npm, yarn, pnpm (external tools) | Built-in (deno add, deno install) | Built-in (bun install -- fastest available) |
| Security model | No sandbox; full system access by default; experimental --experimental-permission in Node 20+ | Secure by default; explicit permission flags (--allow-read, --allow-net, etc.) | No sandbox; full system access by default |
| Standard library | Core modules (fs, path, http, crypto, etc.) | Comprehensive @std/ on JSR (fs, path, http, async, testing, etc.) | Minimal; relies on npm ecosystem and Node.js compat |
| npm compatibility | Native (the canonical npm runtime) | Full via npm: specifier and package.json (Deno 2.x) | Native; drop-in Node.js replacement for most packages |
| Module system | CommonJS + ESM (dual system) | ESM-only; URL imports, import maps, JSR | ESM + CommonJS (transparent interop) |
| HTTP server | http.createServer, http2 module | Deno.serve (web standard Request/Response) | Bun.serve (optimized, web standard Request/Response) |
| Test runner | Built-in node --test (basic, Node 18+) | Built-in deno test (full-featured, BDD, mocking, snapshots, coverage) | Built-in bun test (Jest-compatible API) |
| Bundler | None built-in (use esbuild, Vite, webpack, Rollup) | Built-in deno bundle (deprecated in favor of esbuild/Rollup) | Built-in bun build (fast, esbuild-compatible API) |
| Formatter | None built-in (use Prettier) | Built-in deno fmt | None built-in (use Prettier) |
| Linter | None built-in (use ESLint) | Built-in deno lint | None built-in (use ESLint) |
| Edge deployment | Various (Vercel, AWS Lambda, Cloudflare via adapters) | Deno Deploy (V8 isolates, global edge network) | None (self-hosted or via adapters) |
| Single executable | --experimental-sea-config (Node 20+) | deno compile (cross-platform) | bun build --compile |
| Web standard APIs | Partial (fetch in v18+, crypto.subtle, AbortController) | Comprehensive (fetch, Request, Response, Web Crypto, Streams, URLPattern, BroadcastChannel) | Comprehensive (fetch, Request, Response, Web Crypto, Streams) |
| Watch mode | --watch (Node 18+) | --watch flag on most commands | --watch flag |
| REPL | node | deno (TypeScript-aware) | bun |
| Startup speed | Moderate | Fast | Fastest |
| Ecosystem size | Largest (millions of npm packages) | Full npm access + JSR | Full npm access |
| Maturity | Most mature (since 2009) | Stable (since 2018, 2.x since 2024) | Rapidly maturing (since 2022, 1.0 in 2023) |
Choosing a Runtime
Choose Node.js When
- Maximum ecosystem compatibility matters. Node.js is the canonical npm runtime. Every npm package is designed and tested against Node.js first. Native addons (
.nodefiles compiled vianode-gypor N-API) only work in Node.js. - Your infrastructure is built around Node.js. Existing CI/CD pipelines, Docker images, monitoring tools, and deployment platforms assume Node.js.
- You need enterprise-grade frameworks. NestJS, Express, Fastify, and the broader middleware ecosystem are most mature on Node.js.
- Long-term support and stability are critical. Node.js has a well-defined LTS schedule with predictable release cycles (even-numbered releases receive 30 months of LTS support).
- Your team has deep Node.js expertise. The debugging tools, profiling workflows, and operational knowledge are well established.
Choose Deno When
- Security is a priority. Deno's explicit permission model prevents unauthorized file, network, and environment access by default.
- You want zero-config TypeScript. Deno runs
.tsfiles natively with no transpilation step, notsconfig.jsonrequired, and a built-in LSP. - You prefer web standard APIs. Deno's API surface mirrors the browser (
fetch,Request,Response,ReadableStream,crypto.subtle) making code portable between server and edge. - You want built-in tooling. Formatter, linter, test runner, benchmarking, and documentation generation are all included.
- You are deploying to Deno Deploy. The Deno Deploy edge platform provides globally distributed V8 isolates with Deno KV for data persistence.
- You want a modern module system. ESM-only with import maps eliminates the CJS/ESM dual-module confusion.
Choose Bun When
- Startup speed and throughput are paramount. Bun's JavaScriptCore engine and Zig implementation deliver the fastest cold starts and HTTP throughput among the three runtimes.
- You want an all-in-one tool. Bun combines runtime, package manager, bundler, and test runner into a single binary.
- You need the fastest package manager.
bun installis significantly faster than npm, yarn, or pnpm for both clean installs and cached installs. - You want Jest-compatible testing without config.
bun testimplements the Jest API natively, allowing existing test suites to run with minimal changes. - You are building performance-sensitive applications. Bun's optimized HTTP server, SQLite bindings, and file I/O are designed for maximum throughput.
- You need a drop-in Node.js replacement. Bun aims for full Node.js API compatibility and can run most Node.js projects without modification.
Decision Matrix
| Scenario | Recommended Runtime | Reason |
|---|---|---|
| Enterprise REST API with Express/NestJS | Node.js | Widest framework support and deployment options |
| New TypeScript project with security requirements | Deno | Secure by default, native TypeScript, built-in tools |
| Edge functions with global data persistence | Deno | Deno Deploy + Deno KV |
| High-throughput microservice | Bun | Fastest HTTP server and startup time |
| Monorepo with many packages needing fast installs | Bun | Fastest package manager |
| CLI tool distributed as single binary | Deno or Bun | Both offer excellent single-executable compilation |
| Legacy Node.js project maintenance | Node.js | No migration risk |
| Greenfield full-stack web app | Deno or Node.js | Deno for Fresh/modern stack; Node.js for Next.js/Remix |
| Script/automation with minimal setup | Deno or Bun | Both run TypeScript with zero config |
| Jupyter notebook data exploration | Deno | Built-in deno jupyter kernel |
Multi-Runtime Development
Writing Runtime-Agnostic Code
Code that targets multiple runtimes should rely on web standard APIs and avoid runtime-specific globals.
// Portable HTTP handler -- works in Node.js (with adapter), Deno, and Bun
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/api/hello") {
return Response.json({ message: "Hello, World!" });
}
return new Response("Not Found", { status: 404 });
},
};
Shared APIs Across Runtimes
| API | Node.js | Deno | Bun |
|---|---|---|---|
fetch | v18+ (global) | Always available | Always available |
Request / Response | v18+ (global) | Always available | Always available |
URL / URLSearchParams | v10+ (global) | Always available | Always available |
AbortController | v15+ (global) | Always available | Always available |
crypto.subtle | v15+ (globalThis.crypto) | Always available | Always available |
ReadableStream / WritableStream | v18+ (global) | Always available | Always available |
TextEncoder / TextDecoder | v11+ (global) | Always available | Always available |
structuredClone | v17+ (global) | Always available | Always available |
performance.now() | v8+ (perf_hooks or global) | Always available | Always available |
setTimeout / setInterval | Always available | Always available | Always available |
console | Always available | Always available | Always available |
Conditional Runtime Detection
// Detect which runtime is executing
function detectRuntime(): "node" | "deno" | "bun" | "unknown" {
if (typeof Deno !== "undefined") return "deno";
if (typeof Bun !== "undefined") return "bun";
if (typeof process !== "undefined" && process.versions?.node) return "node";
return "unknown";
}
// Runtime-specific file reading
async function readFile(path: string): Promise<string> {
const runtime = detectRuntime();
switch (runtime) {
case "deno":
return await Deno.readTextFile(path);
case "bun":
return await Bun.file(path).text();
case "node": {
const { readFile } = await import("node:fs/promises");
return await readFile(path, "utf-8");
}
default:
throw new Error(`Unsupported runtime: ${runtime}`);
}
}
Frameworks That Abstract the Runtime
Several frameworks provide a unified API across runtimes, removing the need to write runtime-specific code:
| Framework | Runtimes Supported | Use Case |
|---|---|---|
| Hono | Node.js, Deno, Bun, Cloudflare Workers, AWS Lambda | Lightweight web framework with web standard APIs |
| Elysia | Bun (primary), Node.js (via adapter) | High-performance web framework optimized for Bun |
| Nitro | Node.js, Deno, Bun, Cloudflare, Vercel, Netlify | Universal server engine (powers Nuxt) |
| h3 | Node.js, Deno, Bun, Cloudflare Workers | Minimal HTTP framework with universal compatibility |
Best Practices
-
Default to web standard APIs when writing code that may run on multiple runtimes. Use
fetch,Request,Response,URL,crypto.subtle, andReadableStreaminstead of runtime-specific equivalents. -
Choose the runtime that matches your deployment target. If deploying to Deno Deploy, use Deno. If deploying to AWS Lambda or Vercel, Node.js typically has the best support. If self-hosting for maximum performance, consider Bun.
-
Use a cross-runtime framework (Hono, Nitro, h3) if you anticipate needing to switch runtimes or deploy to multiple targets.
-
Lock your runtime version in CI/CD using version managers (
nvm,fnm,mise,asdf) or Docker images to ensure reproducible builds. -
Test on your target runtime. Even with high compatibility, subtle differences exist in behavior (especially around
node_modulesresolution, native addons, and edge cases in stream handling). -
Use TypeScript strict mode regardless of runtime. All three runtimes benefit equally from stricter type checking.
-
Follow each runtime's LTS/stability schedule for production deployments. Node.js even-numbered releases get LTS; Deno 2.x has LTS releases; Bun follows semver.
-
Evaluate the tooling story holistically. Deno includes formatter, linter, and test runner. Node.js requires external tools for each. Bun includes a test runner and bundler but not a formatter or linter. Factor the total setup cost into your decision.
-
Consider the security requirements of your environment. If running untrusted code or operating in a compliance-sensitive context, Deno's permission model provides meaningful defense in depth.
-
Benchmark with your actual workload before choosing based on synthetic benchmarks. Real-world performance depends on I/O patterns, dependency graphs, and application architecture more than micro-benchmarks.
