Deno 2 and the Node.js Migration Wave: Is the Switch Worth It?
on Deno, Node.js, Javascript, Typescript, Runtime, Backend
The Runtime War We Didn’t Expect
JavaScript server-side was Node.js. Full stop. Then Bun appeared and claimed to be 3x faster. Then Deno 2 shipped full Node.js compatibility and a radically better developer experience. Then Node.js responded with built-in TypeScript support and improved security features.
In 2026, the JavaScript server runtime landscape is genuinely competitive for the first time in 15 years. And the answer to “which one should I use?” is more nuanced than anyone expected.
Photo by Claudio Schwarz on Unsplash
What Changed with Deno 2
Deno’s original pitch — a reimagined Node.js with TypeScript by default, better security, and Web APIs — was technically excellent and practically difficult to adopt. The breaking change: Deno 1.x didn’t run npm packages well.
Deno 2 fixed that. Completely.
npm Compatibility That Actually Works
# Import npm packages natively — no package.json needed
deno run --allow-net main.ts
// main.ts
import express from "npm:express@4";
import { z } from "npm:zod@3";
const app = express();
const UserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
app.post("/users", express.json(), (req, res) => {
const result = UserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json(result.error);
}
res.json({ created: result.data });
});
app.listen(3000);
This is not a polyfill or compatibility shim. It’s running Express, Zod, and virtually any npm package as-is. The entire npm ecosystem is available.
The deno.json Workspace
Deno 2 introduced workspaces and a proper project config:
{
"workspace": ["./packages/*"],
"imports": {
"@std/": "jsr:@std/",
"zod": "npm:zod@^3.22.0"
},
"tasks": {
"dev": "deno run --watch --allow-net --allow-env src/main.ts",
"test": "deno test --allow-net",
"build": "deno compile --output dist/server src/main.ts",
"lint": "deno lint",
"fmt": "deno fmt"
},
"lint": {
"rules": {
"tags": ["recommended"],
"include": ["no-console"]
}
},
"fmt": {
"lineWidth": 100,
"singleQuote": true
}
}
One config file replaces package.json + tsconfig.json + .eslintrc + .prettierrc. The built-in toolchain (formatter, linter, test runner) means zero configuration for 90% of projects.
The Security Model: Actually Meaningful
Node.js added a permissions model in v22, but it’s opt-in and incomplete. Deno’s security model is fundamental:
# Default: no permissions at all
deno run script.ts
# Grant only what's needed
deno run \
--allow-net=api.stripe.com,api.github.com \
--allow-read=/tmp/uploads \
--allow-env=DATABASE_URL,JWT_SECRET \
payment-processor.ts
# This script cannot:
# - Read arbitrary files
# - Make requests to unexpected hosts
# - Access other env variables
# - Write files (unless explicitly allowed)
# - Access the network beyond the allowed hosts
For security-sensitive applications — payment processors, data pipelines, anything handling PII — this is not a nice-to-have. It’s a meaningful defense against supply chain attacks and compromised dependencies.
Compare with npm: npm install and the package has full access to your filesystem, network, and environment variables. Indefinitely.
Performance: Where Things Actually Stand
Benchmarks in 2026 (realistic HTTP + database workload, p50/p99 latency):
| Runtime | p50 Latency | p99 Latency | Throughput (req/s) |
|---|---|---|---|
| Bun + Hono | 0.8ms | 4.2ms | 127,000 |
| Deno + Oak | 1.1ms | 5.8ms | 98,000 |
| Node.js 22 + Fastify | 1.4ms | 7.1ms | 82,000 |
| Node.js 22 + Express | 2.3ms | 12.4ms | 51,000 |
Important caveats:
- These differences matter at scale; most applications aren’t at that scale
- Database query time dwarfs runtime overhead for typical CRUD apps
- Developer productivity and ecosystem familiarity often matter more than raw numbers
- Bun’s V8 replacement (JavaScriptCore) has different trade-offs in real workloads
The Killer Feature: deno compile
One underappreciated capability: Deno can compile TypeScript to a single binary:
# Compile to a single self-contained executable
deno compile \
--allow-net \
--allow-read \
--output my-api-server \
src/main.ts
# Result: a ~85MB binary that runs anywhere with no runtime installed
ls -lh my-api-server
# -rwxr-xr-x 1 user staff 84M my-api-server
# Run on any Linux server (even without Deno installed)
./my-api-server
For distributing CLIs, shipping single-binary microservices in Docker (FROM scratch), or building tools that run in CI environments without installing runtimes — this is remarkable.
JSR: The Better npm Registry
Deno’s JavaScript Registry (JSR) is now widely used:
// Import from JSR — TypeScript-first, no @types/* needed
import { assert, assertEquals } from "jsr:@std/assert@^1.0.0";
import { parseArgs } from "jsr:@std/cli@^1.0.0";
import { join } from "jsr:@std/path@^1.0.0";
JSR packages:
- Are TypeScript-first (no DefinitelyTyped needed)
- Support all runtimes (Deno, Node.js, Bun, browser)
- Are scored and reviewed for quality
- Have immutable versions (no left-pad incidents)
The @std/ namespace is the Deno standard library, now published on JSR and usable from any runtime.
Migration Guide: Node.js → Deno
For a typical Express API:
Step 1: Check Compatibility
# Deno will tell you what needs attention
deno check --unstable-node-globals src/app.ts
Step 2: Update Entry Point
// Before (Node.js)
const express = require('express');
const app = express();
// After (Deno - works too!)
import express from "npm:express";
const app = express();
// Or migrate to Deno's native HTTP
Deno.serve({ port: 3000 }, async (req) => {
const url = new URL(req.url);
if (url.pathname === "/health") {
return new Response(JSON.stringify({ status: "ok" }), {
headers: { "Content-Type": "application/json" }
});
}
return new Response("Not Found", { status: 404 });
});
Step 3: Update Configuration
// Replace package.json + tsconfig.json with deno.json
{
"tasks": {
"dev": "deno run --watch --allow-net --allow-env src/main.ts",
"start": "deno run --allow-net --allow-env src/main.ts",
"test": "deno test"
}
}
Step 4: Migrate Tests
// Deno has built-in testing — no Jest/Vitest needed
import { assertEquals, assertRejects } from "jsr:@std/assert";
import { describe, it } from "jsr:@std/testing/bdd";
describe("UserService", () => {
it("should create a user", async () => {
const user = await UserService.create({ name: "Alice", email: "alice@example.com" });
assertEquals(user.name, "Alice");
});
it("should reject invalid email", async () => {
await assertRejects(
() => UserService.create({ name: "Bob", email: "not-an-email" }),
Error,
"Invalid email"
);
});
});
When to Migrate vs. Stay
Migrate to Deno when:
- Starting a new project (no migration cost)
- Security model matters (financial services, healthcare)
- You want zero-config TypeScript + toolchain
- Building CLIs or standalone tools (
deno compile) - Tired of managing
tsconfig.json+ linter + formatter separately
Stay on Node.js when:
- Large existing codebase with complex build setup
- Using packages with native addons (some don’t work on Deno/Bun)
- Team is deeply Node.js-specialized
- Using frameworks with no Deno path (some enterprise frameworks)
Consider Bun when:
- Raw performance is the primary constraint
- Running on servers where startup time matters (serverless/Lambda)
- Testing — Bun’s test runner is exceptionally fast
The Honest Assessment
Deno 2 is genuinely excellent. The developer experience is the best in the JavaScript ecosystem. The security model is right. JSR is better than npm.
But Node.js is not going away. The ecosystem inertia is enormous, Node.js 22+ has addressed many of Deno’s original advantages (built-in TypeScript stripping, better security, Fetch API), and most organizations can’t justify migration cost for established services.
The realistic 2026 answer: use Deno for new TypeScript projects where you control the stack. Keep existing Node.js applications on Node.js unless there’s a specific reason to migrate.
The competition has made all JavaScript runtimes better. That’s a win for everyone.
Try Deno without commitment: curl -fsSL https://deno.land/install.sh | sh and run deno repl to explore.
이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)
