MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Introduction to Deno 2.0: The Modern JavaScript Runtime

Explore Deno 2.0: npm compatibility, improved performance, new APIs, and Node.js comparison.

DenoRuntimeTypeScriptJavaScript

By MinhVo

Introduction

Deno 2.0 represents a major milestone in the evolution of JavaScript runtimes. Created by Ryan Dahl—Node.js's original creator—Deno was announced in 2018 as a "secure by default" runtime that addressed many of Node.js's design regrets. While Deno 1.x proved the concept, it struggled with adoption due to lack of npm compatibility. Deno 2.0 changes everything: it brings full npm support, backwards compatibility with the Node.js ecosystem, improved performance, and a mature package management system while maintaining Deno's core principles of security, TypeScript-first development, and web-standard APIs.

The release of Deno 2.0 is significant because it eliminates the primary barrier to adoption. Developers no longer need to choose between Deno's superior developer experience and the vast npm ecosystem. With Deno 2.0, you can import express from 'npm:express' and use any npm package directly, without a node_modules folder, without package.json, and without a build step for TypeScript. This combination of npm compatibility with Deno's security model and modern APIs creates a compelling alternative to Node.js.

In this comprehensive guide, we'll explore Deno 2.0's architecture, understand its security model, learn how npm compatibility works under the hood, build practical applications, and compare Deno 2.0 with Node.js and Bun to help you decide when to use each runtime.

Modern JavaScript runtime and development tools

Understanding Deno 2.0: Core Architecture

Deno 2.0 is built on the V8 JavaScript engine (the same engine powering Chrome and Node.js) and written in Rust. The runtime uses Tokio, an asynchronous runtime for Rust, to handle I/O operations efficiently. This architecture provides several advantages: memory safety from Rust, fast startup times, and the ability to compile to native code.

Security by Default

Unlike Node.js, where any script has full access to the file system, network, and environment variables, Deno runs scripts in a sandbox with no permissions by default. To access the file system, network, environment variables, or spawn child processes, you must explicitly grant permissions using flags:

# No permissions (default) - script runs in sandbox
deno run script.ts
 
# Grant specific permissions
deno run --allow-read=./data --allow-net=api.example.com script.ts
 
# Grant all permissions (like Node.js behavior)
deno run --allow-all script.ts
 
# Interactive permission prompts
deno run --allow-read --prompt script.ts

This security model is critical in today's supply chain attack landscape. When you run a third-party npm package, you trust it with your entire system. Deno's permission system limits what each script can do, providing defense in depth against malicious packages.

TypeScript First

Deno natively supports TypeScript without any configuration or build step. The runtime uses SWC (a fast Rust-based compiler) to transpile TypeScript to JavaScript on the fly. Type checking is performed by the built-in deno check command, which uses the TypeScript compiler for type analysis. This means you can write .ts files and run them directly:

# Run TypeScript directly - no tsc, no tsconfig, no build step
deno run server.ts
 
# Type check without running
deno check server.ts
 
# Run with JSX/TSX support
deno run --jsx react --jsx-import-source react component.tsx

Web Standard APIs

Deno prioritizes web-standard APIs over proprietary ones. You use fetch() instead of http.request(), WebSockets instead of ws, and TextEncoder/TextDecoder instead of Buffer. This means code written for Deno is more portable across runtimes—browser, Deno, Cloudflare Workers, and even Node.js (which increasingly supports web standards).

Security and permissions model architecture

Architecture and Design Patterns

Module Resolution

Deno 2.0 supports three module resolution strategies: URL imports (https://deno.land/std/path.ts), npm specifiers (npm:lodash), and bare specifiers via import maps. Import maps map bare specifiers to URLs, enabling Node.js-like import syntax:

// deno.json
{
  "imports": {
    "lodash": "npm:lodash@4.17.21",
    "@std/path": "jsr:@std/path",
    "express": "npm:express@4.18.2"
  }
}

The Deno Namespace API

Deno exposes platform-specific APIs through the Deno namespace: Deno.readTextFile(), Deno.writeTextFile(), Deno.listen(), Deno.connect(), and more. These APIs are designed with TypeScript in mind, providing excellent type safety and IDE support.

JSR: The JavaScript Registry

Deno 2.0 introduces JSR (jsr.io), a modern package registry designed for TypeScript-first modules. JSR packages are published as source code (not compiled), support automatic npm compatibility, and provide built-in documentation generation. JSR is interoperable with Deno, Node.js, Bun, and edge runtimes.

Step-by-Step Implementation

Let's build practical applications with Deno 2.0 to explore its features.

Setting Up a Deno Project

# Install Deno 2.0
curl -fsSL https://deno.land/install.sh | sh
# Or with Homebrew
brew install deno
 
# Create a project
mkdir my-deno-app && cd my-deno-app
 
# Initialize with deno.json
deno init
// deno.json - project configuration
{
  "name": "my-deno-app",
  "version": "1.0.0",
  "tasks": {
    "dev": "deno run --watch --allow-net --allow-read server.ts",
    "test": "deno test --allow-net --allow-read",
    "build": "deno compile --allow-net --allow-read server.ts",
    "lint": "deno lint",
    "fmt": "deno fmt"
  },
  "imports": {
    "oak": "npm:oak@16.1.0",
    "postgres": "npm:postgres@3.4.3",
    "dotenv": "npm:dotenv@16.3.1"
  },
  "compilerOptions": {
    "strict": true,
    "lib": ["deno.window"]
  },
  "lint": {
    "rules": {
      "tags": ["recommended"]
    }
  },
  "fmt": {
    "options": {
      "indentWidth": 2,
      "singleQuote": true
    }
  }
}

Building a REST API with Oak

// server.ts
import { Application, Router, Context } from "oak";
 
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}
 
const users = new Map<string, User>();
 
const router = new Router();
 
router
  .get("/api/users", (ctx: Context) => {
    const userList = Array.from(users.values());
    ctx.response.body = { data: userList, total: userList.length };
  })
  .get("/api/users/:id", (ctx: Context) => {
    const user = users.get(ctx.params.id);
    if (!user) {
      ctx.response.status = 404;
      ctx.response.body = { error: "User not found" };
      return;
    }
    ctx.response.body = { data: user };
  })
  .post("/api/users", async (ctx: Context) => {
    const body = await ctx.request.body.json();
    const id = crypto.randomUUID();
    const user: User = {
      id,
      name: body.name,
      email: body.email,
      createdAt: new Date(),
    };
    users.set(id, user);
    ctx.response.status = 201;
    ctx.response.body = { data: user };
  })
  .put("/api/users/:id", async (ctx: Context) => {
    const existing = users.get(ctx.params.id);
    if (!existing) {
      ctx.response.status = 404;
      ctx.response.body = { error: "User not found" };
      return;
    }
    const body = await ctx.request.body.json();
    const updated = { ...existing, ...body, id: existing.id };
    users.set(ctx.params.id, updated);
    ctx.response.body = { data: updated };
  })
  .delete("/api/users/:id", (ctx: Context) => {
    if (!users.delete(ctx.params.id)) {
      ctx.response.status = 404;
      ctx.response.body = { error: "User not found" };
      return;
    }
    ctx.response.status = 204;
  });
 
const app = new Application();
 
// CORS middleware
app.use(async (ctx, next) => {
  ctx.response.headers.set("Access-Control-Allow-Origin", "*");
  ctx.response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
  ctx.response.headers.set("Access-Control-Allow-Headers", "Content-Type");
  if (ctx.request.method === "OPTIONS") {
    ctx.response.status = 204;
    return;
  }
  await next();
});
 
// Error handling middleware
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (error) {
    ctx.response.status = 500;
    ctx.response.body = { error: "Internal server error" };
    console.error("Unhandled error:", error);
  }
});
 
app.use(router.routes());
app.use(router.allowedMethods());
 
const port = parseInt(Deno.env.get("PORT") || "8000");
console.log(`Server running on http://localhost:${port}`);
await app.listen({ port });

Using npm Packages

// Using npm packages directly - no node_modules needed
import express from "npm:express@4.18.2";
import _ from "npm:lodash@4.17.21";
import { z } from "npm:zod@3.22.4";
 
const app = express();
app.use(express.json());
 
const UserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().int().min(0).max(150).optional(),
});
 
app.post("/users", (req: any, res: any) => {
  const result = UserSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ errors: result.error.issues });
  }
 
  const user = {
    id: crypto.randomUUID(),
    ...result.data,
    createdAt: new Date().toISOString(),
  };
 
  res.status(201).json(user);
});
 
// GroupBy with lodash
const users = [
  { name: "Alice", role: "admin" },
  { name: "Bob", role: "user" },
  { name: "Charlie", role: "admin" },
];
 
const grouped = _.groupBy(users, "role");
console.log(grouped);
 
app.listen(3000, () => console.log("Express on Deno running!"));

Database Integration with Postgres

// db.ts
import postgres from "postgres";
 
const sql = postgres({
  host: Deno.env.get("DB_HOST") || "localhost",
  port: parseInt(Deno.env.get("DB_PORT") || "5432"),
  database: Deno.env.get("DB_NAME") || "myapp",
  username: Deno.env.get("DB_USER") || "postgres",
  password: Deno.env.get("DB_PASSWORD") || "",
  max: 10, // Connection pool size
});
 
// Type-safe query builder
interface User {
  id: string;
  name: string;
  email: string;
  created_at: Date;
}
 
export async function getUsers(): Promise<User[]> {
  return await sql<User[]>`SELECT * FROM users ORDER BY created_at DESC`;
}
 
export async function getUserById(id: string): Promise<User | null> {
  const [user] = await sql<User[]>`SELECT * FROM users WHERE id = ${id}`;
  return user ?? null;
}
 
export async function createUser(name: string, email: string): Promise<User> {
  const [user] = await sql<User[]>`
    INSERT INTO users (name, email) VALUES (${name}, ${email})
    RETURNING *
  `;
  return user;
}
 
export async function initializeDatabase(): Promise<void> {
  await sql`
    CREATE TABLE IF NOT EXISTS users (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      name VARCHAR(255) NOT NULL,
      email VARCHAR(255) UNIQUE NOT NULL,
      created_at TIMESTAMP DEFAULT NOW()
    )
  `;
}

Compiling to a Standalone Binary

# Compile your Deno app to a single executable
# No Deno installation needed on the target machine
deno compile --allow-net --allow-read --output my-server server.ts
 
# Cross-compile for different platforms
deno compile --target x86_64-unknown-linux-gnu --output my-server-linux server.ts
deno compile --target x86_64-apple-darwin --output my-server-macos server.ts
deno compile --target x86_64-pc-windows-msvc --output my-server.exe server.ts

Runtime performance and benchmarking

Real-World Use Cases

Use Case 1: Serverless Edge Functions

Deno powers serverless functions on platforms like Deno Deploy, Netlify Edge Functions, and Supabase Edge Functions. These platforms run Deno at the edge, executing your code in data centers close to users for minimal latency. Deno Deploy offers 100K free requests per day, making it ideal for personal projects and prototypes.

Use Case 2: CLI Tools and Scripts

Deno's single-binary compilation, built-in testing, and TypeScript support make it excellent for CLI tools. You can distribute a single executable that requires no runtime installation. The Deno standard library provides file system operations, HTTP clients, and argument parsing out of the box.

Use Case 3: Full-Stack Web Applications

With frameworks like Fresh (built on Deno), you can build full-stack web applications with server-side rendering, islands architecture, and edge deployment. Fresh leverages Deno's TypeScript-first approach and web-standard APIs for a modern development experience.

Use Case 4: API Microservices

Deno's fast startup time (50-100ms) and low memory footprint make it suitable for microservices deployed in containers. Combined with npm compatibility, you can use established libraries like Express, Koa, or Hono while benefiting from Deno's security model and TypeScript support.

Best Practices for Production

  1. Use deno.json for project configuration: Define tasks, import maps, compiler options, lint rules, and formatting preferences in deno.json. This serves as both tsconfig.json and package.json.

  2. Pin dependency versions: Use exact versions in import maps ("lodash": "npm:lodash@4.17.21") to prevent unexpected updates. Use deno.lock for deterministic installs.

  3. Use JSR for Deno-native packages: Prefer JSR packages over deno.land/x modules. JSR provides better TypeScript support, automatic npm compatibility, and built-in documentation.

  4. Grant minimal permissions: Only grant the permissions your application actually needs. Use --allow-read=./data instead of --allow-read, and --allow-net=api.example.com instead of --allow-net.

  5. Use the built-in formatter and linter: Run deno fmt and deno lint as part of your CI pipeline. The linter catches common errors, and the formatter ensures consistent style.

  6. Write tests with deno test: Deno has a built-in test runner that supports async tests, mocking, and code coverage. Use Deno.test() instead of external test frameworks.

  7. Use deno compile for distribution: Compile your application to a standalone binary for easy distribution. This eliminates the need for Deno installation on target machines.

  8. Monitor with structured logging: Use console.log with structured JSON for production logging. Combine with log aggregation services for observability.

Common Pitfalls and Solutions

PitfallImpactSolution
Using --allow-all in productionNo security benefit, defeats Deno's purposeGrant specific permissions: --allow-read, --allow-net=hostname
Not pinning npm versionsUnexpected breaking changes from updatesUse exact versions in import maps and commit deno.lock
Using Node.js-specific APIs without checkingRuntime errors in DenoUse web-standard APIs when possible; check compatibility in docs
Ignoring the linterMissing potential bugs and security issuesRun deno lint in CI and fix all warnings
Not using deno check before deploymentType errors in productionAdd deno check to your CI/CD pipeline

Performance Optimization

// Deno performance optimization techniques
 
// 1. Use Deno.Kv for key-value storage (built-in)
const kv = await Deno.openKv();
 
await kv.set(["users", "alice"], { name: "Alice", score: 100 });
const result = await kv.get(["users", "alice"]);
console.log(result.value); // { name: "Alice", score: 100 }
 
// 2. Use Web Workers for CPU-intensive tasks
const worker = new Worker(new URL("./worker.ts", import.meta.url).href, {
  type: "module",
});
 
worker.postMessage({ data: largeDataset });
worker.onmessage = (e) => {
  console.log("Processed result:", e.data);
};
 
// 3. Connection pooling for database access
import postgres from "npm:postgres";
const sql = postgres({ max: 20, idle_timeout: 30 });
 
// 4. Use streaming for large responses
async function streamLargeFile(ctx: Context) {
  const file = await Deno.open("./large-file.csv", { read: true });
  ctx.response.headers.set("Content-Type", "text/csv");
  ctx.response.headers.set("Content-Disposition", "attachment; filename=data.csv");
  ctx.response.body = file.readable;
}
 
// 5. Cache frequently accessed data
const cache = new Map<string, { data: any; expiry: number }>();
 
async function getCached<T>(key: string, ttl: number, fetcher: () => Promise<T>): Promise<T> {
  const entry = cache.get(key);
  if (entry && entry.expiry > Date.now()) return entry.data as T;
 
  const data = await fetcher();
  cache.set(key, { data, expiry: Date.now() + ttl });
  return data;
}

Comparison with Alternatives

FeatureDeno 2.0Node.js 22Bun
TypeScriptNative (no build step)Requires tsx/ts-nodeNative
npm compatibilityFull (npm: prefix)NativeFull
Security modelPermission-based (sandbox)Full accessFull access
Package managerBuilt-innpm/yarn/pnpmBuilt-in
Built-in toolingFormatter, linter, test runnerNone (external)Bundler, test runner
Web standard APIsFull supportPartialPartial
Startup timeFast (~50ms)Medium (~150ms)Very fast (~10ms)
Single binary compileYesYes (SEA)Yes
Edge deploymentDeno Deploy, NetlifyLimitedCloudflare Workers
Ecosystem maturityGrowingMassiveGrowing
Learning curveLowLowLow

Deno 2.0 excels when you want TypeScript-first development with security guarantees and web-standard APIs. Node.js remains the safest choice for production applications due to its massive ecosystem and battle-tested stability. Bun offers the fastest performance but has a smaller ecosystem and less production validation.

Advanced Patterns and Techniques

Using Deno.Kv for Distributed State

// Deno.Kv is a globally distributed key-value store
// available on Deno Deploy
const kv = await Deno.openKv();
 
// Atomic transactions
await kv.atomic()
  .set(["counters", "page_views"], 0)
  .set(["counters", "signups"], 0)
  .commit();
 
// Listeners for real-time updates
kv.watch([["counters", "page_views"]]).forEach((entry) => {
  console.log("Page views updated:", entry.value);
});
 
// Queue for background processing
await kv.enqueue({ task: "send_email", to: "user@example.com" });
 
// Process queue items
const queue = kv.listenQueue(async (msg: any) => {
  if (msg.task === "send_email") {
    await sendEmail(msg.to, "Welcome!", "Thanks for signing up.");
  }
});

Building with Fresh Framework

// routes/index.tsx - Fresh framework page
import { useSignal } from "https://deno.land/x/fresh@1.6.0/runtime.ts";
 
export default function Home() {
  const count = useSignal(0);
 
  return (
    <div>
      <h1>Deno Fresh Counter</h1>
      <p>Count: {count}</p>
      <button onClick={() => count.value++}>Increment</button>
    </div>
  );
}

Testing Strategies

// Testing with Deno's built-in test runner
import { assertEquals, assertRejects } from "https://deno.land/std/assert/mod.ts";
import { getUserById, createUser, initializeDatabase } from "./db.ts";
 
Deno.test("createUser creates a new user", async () => {
  await initializeDatabase();
  const user = await createUser("Test User", "test@example.com");
 
  assertEquals(user.name, "Test User");
  assertEquals(user.email, "test@example.com");
  assertEquals(typeof user.id, "string");
});
 
Deno.test("getUserById returns null for nonexistent user", async () => {
  const user = await getUserById("nonexistent-id");
  assertEquals(user, null);
});
 
// Mocking
Deno.test("API endpoint returns user list", async () => {
  // Use Deno's built-in mock utilities
  const fetchSpy = spy(globalThis, "fetch");
 
  const response = await fetch("http://localhost:8000/api/users");
  assertEquals(response.status, 200);
 
  const data = await response.json();
  assertEquals(Array.isArray(data.data), true);
 
  fetchSpy.restore();
});
 
// Snapshot testing
Deno.test("API response matches snapshot", async () => {
  const response = await fetch("http://localhost:8000/api/users");
  const data = await response.json();
  await assertSnapshot(data);
});

Future Outlook

Deno 2.0 positions itself as the "Node.js done right" runtime. The addition of npm compatibility removes the biggest adoption barrier, while JSR provides a modern package registry designed for the TypeScript era. Deno Deploy's global edge network makes deploying Deno applications to 35+ regions trivial.

The Deno company continues to invest in Fresh (their web framework), Deno KV (globally distributed state), and Deno Subhosting (allowing platforms to safely run user code). As the JavaScript ecosystem increasingly adopts TypeScript and web standards, Deno's architectural decisions prove prescient.

Conclusion

Deno 2.0 is a production-ready JavaScript runtime that combines the best ideas from Node.js with modern improvements: TypeScript without configuration, npm compatibility without node_modules, security without performance penalties, and web-standard APIs without polyfills. The key takeaways are: Deno's permission model provides defense-in-depth against supply chain attacks by sandboxing code execution. npm compatibility means you can use any npm package directly without a package manager or node_modules directory.

TypeScript is a first-class citizen—write .ts files and run them directly with no build step. The built-in toolchain (formatter, linter, test runner, bundler) eliminates the need for external dependencies. Start by installing Deno 2.0, creating a simple HTTP server, and exploring the standard library at docs.deno.com. For production deployments, consider Deno Deploy for edge computing or compile to a standalone binary for container-based deployments.