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

Deno 2.0: A Serious Node.js Alternative

Deno 2.0 with npm compatibility, improved performance, and the Fresh framework.

DenoRuntimeJavaScriptTypeScript

By MinhVo

Introduction

Deno 1.0 launched in 2020 with bold promises: TypeScript-first, secure by default, no package manager, and web-standard APIs. It was a compelling vision, but the lack of npm compatibility made it impractical for most production applications. Developers loved the philosophy but couldn't abandon the npm ecosystem.

Deno 2.0, released in late 2024, changes everything. Full npm compatibility means you can use any Node.js package without modification. Native TypeScript execution without configuration. Built-in testing, formatting, linting, and benchmarking. A package manager that works with both Deno and npm modules. And the Fresh framework for building full-stack web applications with zero client-side JavaScript by default.

This guide covers what's new in Deno 2.0, how to migrate from Node.js, the Fresh framework for full-stack development, and the production patterns that make Deno a serious choice for modern applications.

Deno runtime architecture

Understanding Deno 2.0: What's Changed

npm Compatibility

The biggest change in Deno 2.0 is full npm compatibility. You can import any npm package directly using npm: specifiers or install them with the deno add command.

// Import npm packages directly
import express from "npm:express@4.18";
import { PrismaClient } from "npm:prisma/client";
import _ from "npm:lodash@4.17";
 
// Or install them (adds to package.json or deno.json)
// deno add npm:express@4
// deno add npm:prisma@5
 
// Use Node.js built-in modules
import { readFile } from "node:fs/promises";
import { createServer } from "node:http";
import path from "node:path";
 
// Mix Deno and npm modules freely
import { serve } from "https://deno.land/std@0.224/http/server.ts";
import express from "npm:express";

TypeScript Without Configuration

Deno executes TypeScript natively—no tsconfig.json, no build step, no transpilation. It supports the latest TypeScript features including decorators, satisfies operator, and const type parameters.

// Just run it: deno run server.ts
// No tsconfig.json needed!
 
interface User {
  id: string;
  name: string;
  email: string;
}
 
// Type-safe API with native TypeScript
async function getUser(id: string): Promise<User> {
  const response = await fetch(`https://api.example.com/users/${id}`);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}
 
// Decorators work out of the box
function log(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = async function (...args: any[]) {
    console.log(`Calling ${key} with`, args);
    const result = await original.apply(this, args);
    console.log(`${key} returned`, result);
    return result;
  };
}
 
class UserService {
  @log
  async findUser(id: string) {
    return getUser(id);
  }
}

Built-in Toolchain

Deno 2.0 includes a complete development toolchain:

# Format code (prettier-like)
deno fmt src/
 
# Lint code (eslint-like)
deno lint src/
 
# Run tests (jest-like)
deno test src/
 
# Run benchmarks
deno bench src/
 
# Type check without running
deno check src/main.ts
 
# Compile to standalone executable
deno compile --allow-net --allow-read src/main.ts
 
# Bundle for production
deno bundle src/main.ts dist/main.js

Deno 2.0 toolchain overview

Step-by-Step Implementation

Setting Up a Deno 2.0 Project

// deno.json - Project configuration
{
  "tasks": {
    "dev": "deno run --allow-net --allow-read --watch src/main.ts",
    "test": "deno test --allow-net --allow-read src/",
    "fmt": "deno fmt src/",
    "lint": "deno lint src/",
    "build": "deno compile --allow-net --allow-read --output dist/server src/main.ts"
  },
  "imports": {
    "@std/assert": "jsr:@std/assert",
    "@std/http": "jsr:@std/http",
    "express": "npm:express@4",
    "prisma": "npm:prisma@5",
    "@prisma/client": "npm:@prisma/client@5"
  },
  "compilerOptions": {
    "strict": true,
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  },
  "fmt": {
    "options": {
      "useTabs": false,
      "lineWidth": 100,
      "indentWidth": 2
    }
  },
  "lint": {
    "rules": {
      "tags": ["recommended"]
    }
  }
}

Building a REST API with Deno and Hono

// src/main.ts - REST API with Hono (Deno-native web framework)
import { Hono } from "npm:hono@4";
import { cors } from "npm:hono@4/cors";
import { logger } from "npm:hono@4/logger";
import { z } from "npm:zod@3";
 
const app = new Hono();
 
// Middleware
app.use("*", logger());
app.use("*", cors());
 
// Validation schema
const UserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  role: z.enum(["admin", "user", "viewer"]),
});
 
// In-memory store (replace with database in production)
const users = new Map<string, any>();
 
// Routes
app.get("/api/users", (c) => {
  const allUsers = Array.from(users.values());
  return c.json({ users: allUsers, count: allUsers.length });
});
 
app.get("/api/users/:id", (c) => {
  const user = users.get(c.req.param("id"));
  if (!user) return c.json({ error: "Not found" }, 404);
  return c.json(user);
});
 
app.post("/api/users", async (c) => {
  const body = await c.req.json();
  const result = UserSchema.safeParse(body);
 
  if (!result.success) {
    return c.json({ error: result.error.issues }, 400);
  }
 
  const id = crypto.randomUUID();
  const user = { id, ...result.data, createdAt: new Date().toISOString() };
  users.set(id, user);
 
  return c.json(user, 201);
});
 
app.delete("/api/users/:id", (c) => {
  const id = c.req.param("id");
  if (!users.has(id)) return c.json({ error: "Not found" }, 404);
  users.delete(id);
  return c.json({ deleted: true });
});
 
// Health check
app.get("/health", (c) => c.json({ status: "ok", runtime: "deno" }));
 
// Start server
Deno.serve({ port: 3000 }, app.fetch);

Using Deno with Prisma ORM

// src/db.ts - Database access with Prisma
import { PrismaClient } from "@prisma/client";
 
const prisma = new PrismaClient();
 
export async function findUsers(filter?: { role?: string }) {
  return prisma.user.findMany({
    where: filter,
    include: { posts: { take: 5, orderBy: { createdAt: "desc" } } },
  });
}
 
export async function createUser(data: { name: string; email: string; role: string }) {
  return prisma.user.create({ data });
}
 
export async function getUserWithPosts(userId: string) {
  return prisma.user.findUnique({
    where: { id: userId },
    include: {
      posts: {
        orderBy: { createdAt: "desc" },
        include: { tags: true },
      },
    },
  });
}

Deno Fresh framework architecture

Building Full-Stack Apps with Fresh

Fresh is Deno's native web framework for building server-rendered applications with islands of interactivity.

// routes/index.tsx - Fresh page component
import { useSignal } from "@preact/signals";
import { Handlers, PageProps } from "$fresh/server.ts";
import { Head } from "$fresh/runtime.ts";
 
interface User {
  id: string;
  name: string;
  email: string;
}
 
export const handler: Handlers<User[]> = {
  async GET(req, ctx) {
    const users = await fetch("http://localhost:3000/api/users");
    const data = await users.json();
    return ctx.render(data.users);
  },
};
 
export default function HomePage({ data }: PageProps<User[]>) {
  return (
    <>
      <Head>
        <title>User Dashboard</title>
      </Head>
      <div class="max-w-4xl mx-auto p-6">
        <h1 class="text-3xl font-bold mb-6">Users</h1>
        <div class="grid gap-4">
          {data.map((user) => (
            <a
              key={user.id}
              href={`/users/${user.id}`}
              class="block p-4 border rounded-lg hover:shadow-md transition-shadow"
            >
              <h2 class="font-semibold">{user.name}</h2>
              <p class="text-gray-600">{user.email}</p>
            </a>
          ))}
        </div>
      </div>
    </>
  );
}
// islands/Counter.tsx - Interactive island (only JS shipped to client)
import { useSignal } from "@preact/signals";
 
export default function Counter() {
  const count = useSignal(0);
 
  return (
    <div class="flex items-center gap-4">
      <button
        onClick={() => count.value--}
        class="px-4 py-2 bg-gray-200 rounded"
      >
        -
      </button>
      <span class="text-2xl font-mono">{count}</span>
      <button
        onClick={() => count.value++}
        class="px-4 py-2 bg-gray-200 rounded"
      >
        +
      </button>
    </div>
  );
}

Real-World Use Cases

Use Case 1: API Gateway with Deno Deploy

// Deployed to Deno Deploy (serverless Deno runtime)
import { Hono } from "npm:hono@4";
 
const app = new Hono();
 
// Rate limiting middleware
const rateLimit = new Map<string, { count: number; reset: number }>();
 
app.use("*", async (c, next) => {
  const ip = c.req.header("x-forwarded-for") || "unknown";
  const now = Date.now();
  const limit = rateLimit.get(ip);
 
  if (limit && limit.reset > now && limit.count >= 100) {
    return c.json({ error: "Rate limit exceeded" }, 429);
  }
 
  if (!limit || limit.reset <= now) {
    rateLimit.set(ip, { count: 1, reset: now + 60000 });
  } else {
    limit.count++;
  }
 
  await next();
});
 
// Proxy to backend services
app.all("/api/users/*", async (c) => {
  const path = c.req.path.replace("/api/users", "");
  const response = await fetch(`https://users-service.internal${path}`, {
    method: c.req.method,
    headers: c.req.raw.headers,
    body: c.req.raw.body,
  });
  return response;
});
 
app.all("/api/orders/*", async (c) => {
  const path = c.req.path.replace("/api/orders", "");
  const response = await fetch(`https://orders-service.internal${path}`, {
    method: c.req.method,
    headers: c.req.raw.headers,
    body: c.req.raw.body,
  });
  return response;
});
 
Deno.serve(app.fetch);

Use Case 2: Background Job Processing

// src/workers/email.ts - Background worker
import { connect } from "npm:@nats-io/transport-node@1";
 
const nc = await connect({ servers: "nats://localhost:4222" });
const js = nc.jetstream();
 
// Process email queue
const consumer = await js.consumers.get("emails", "email-processor");
const messages = await consumer.fetch({ max_messages: 10 });
 
for await (const msg of messages) {
  try {
    const email = JSON.parse(new TextDecoder().decode(msg.data));
 
    // Send email using Deno's native fetch
    const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${Deno.env.get("SENDGRID_API_KEY")}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        personalizations: [{ to: [{ email: email.to }] }],
        from: { email: "noreply@example.com" },
        subject: email.subject,
        content: [{ type: "text/html", value: email.body }],
      }),
    });
 
    if (response.ok) {
      await msg.ack();
    } else {
      await msg.nak();
    }
  } catch (err) {
    console.error("Email processing failed:", err);
    await msg.nak();
  }
}

Use Case 3: Edge Functions with Deno Deploy

// Edge function: A/B testing at the edge
export default async function handler(req: Request): Promise<Response> {
  const url = new URL(req.url);
  const cookie = req.headers.get("cookie") || "";
 
  // Check for existing A/B assignment
  const existingGroup = cookie.match(/ab_group=([AB])/)?.[1];
 
  let group: string;
  if (existingGroup) {
    group = existingGroup;
  } else {
    // Assign to A or B group (50/50 split)
    group = Math.random() < 0.5 ? "A" : "B";
  }
 
  // Fetch the appropriate variant
  const variantUrl = group === "A"
    ? "https://cdn.example.com/landing-a.html"
    : "https://cdn.example.com/landing-b.html";
 
  const response = await fetch(variantUrl);
  const html = await response.text();
 
  return new Response(html, {
    headers: {
      "Content-Type": "text/html",
      "Set-Cookie": `ab_group=${group}; Path=/; Max-Age=86400`,
      "X-AB-Group": group,
    },
  });
}

Migrating from Node.js to Deno

// Before (Node.js with Express)
const express = require('express');
const app = express();
app.get('/', (req, res) => res.json({ hello: 'world' }));
app.listen(3000);
 
// After (Deno with Hono)
import { Hono } from "npm:hono@4";
const app = new Hono();
app.get("/", (c) => c.json({ hello: "world" }));
Deno.serve({ port: 3000 }, app.fetch);

Best Practices for Production

  1. Use npm: specifiers for existing packages: Don't rewrite working code. Import npm packages directly with npm:package@version.
  2. Use deno.json for project configuration: Centralizes tasks, imports, compiler options, and linter/formatter settings.
  3. Leverage Deno Deploy for edge functions: Zero-config deployment to 35+ edge locations globally.
  4. Use Fresh for server-rendered applications: Islands architecture means minimal client-side JavaScript.
  5. Set permissions explicitly: Use --allow-net, --allow-read, etc. to follow the principle of least privilege.
  6. Use JSR (Deno's package registry) for Deno-first packages: Better TypeScript support and automatic documentation.
  7. Compile to standalone executables: deno compile produces self-contained binaries with no runtime dependency.
  8. Use Deno's built-in testing: No need for Jest, Mocha, or other test frameworks.

Common Pitfalls and Solutions

PitfallImpactSolution
Missing permissionsRuntime errorsUse explicit --allow-* flags
Node.js-specific APIsCompatibility issuesUse node: polyfills (available in Deno 2.0)
CommonJS modulesImport errorsUse npm: specifier (auto-converts CJS)
Missing package.jsonTooling confusionDeno 2.0 reads package.json natively
Different module resolutionImport path errorsUse deno.json imports map for aliases

Performance Optimization

// Use Deno's built-in benchmarking
Deno.bench("JSON parse", () => {
  JSON.parse('{"key": "value"}');
});
 
Deno.bench("fetch request", async () => {
  await fetch("https://httpbin.org/get");
});
 
// Compile-time optimization
// deno compile --target x86_64-unknown-linux-gnu --output server src/main.ts
 
// Use Web Workers for CPU-intensive tasks
const worker = new Worker(new URL("./worker.ts", import.meta.url).href, {
  type: "module",
});
worker.postMessage({ task: "heavy-computation", data: largeDataset });
worker.onmessage = (e) => console.log("Result:", e.data);

Comparison with Alternatives

FeatureDeno 2.0Node.jsBun
TypeScriptNativeVia transpilerNative
npm compatibilityFull (npm:)NativeFull
Package managerBuilt-innpm/yarn/pnpmBuilt-in
Security modelPermission-basedNoneNone
Built-in toolsfmt, lint, test, benchNonetest, bundler
Hot reload--watch flagnodemon--watch
Edge deploymentDeno DeployCloudflare WorkersCloudflare Workers
Web standard APIsFullPartialPartial
Ecosystem maturityGrowingMassiveGrowing

Testing Strategies

// src/utils_test.ts - Deno built-in testing
import { assertEquals, assertThrows } from "@std/assert";
import { getUser, createUser } from "./utils.ts";
 
Deno.test("createUser returns user with ID", async () => {
  const user = await createUser({ name: "Alice", email: "alice@test.com", role: "admin" });
  assertEquals(typeof user.id, "string");
  assertEquals(user.name, "Alice");
});
 
Deno.test("getUser returns null for missing user", async () => {
  const user = await getUser("nonexistent");
  assertEquals(user, null);
});
 
// Benchmark test
Deno.bench("create 1000 users", async () => {
  for (let i = 0; i < 1000; i++) {
    await createUser({ name: `User ${i}`, email: `user${i}@test.com`, role: "user" });
  }
});

Future Outlook

Deno 2.0 has closed the gap with Node.js on npm compatibility while maintaining its security-first philosophy. The JSR package registry is growing rapidly, Deno Deploy is expanding its edge computing capabilities, and Fresh is becoming a serious competitor to Next.js for server-rendered applications. The convergence of Node.js and Deno APIs means choosing between them is increasingly a matter of preference rather than capability.

Production Deployment and Operations

Running backend services in production requires attention to reliability, observability, and operational concerns that don't exist in development environments. Proper deployment practices ensure your service remains available and performant under real-world conditions.

Graceful Shutdown Handling

Implement graceful shutdown to prevent request failures during deployments and restarts:

const server = app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
 
async function gracefulShutdown(signal) {
  console.log(`Received ${signal}, starting graceful shutdown...`);
 
  // Stop accepting new connections
  server.close(async () => {
    console.log('HTTP server closed');
 
    try {
      // Wait for existing requests to complete (with timeout)
      await Promise.race([
        waitForActiveRequests(),
        new Promise((_, reject) =>
          setTimeout(() => reject(new Error('Shutdown timeout')), 30000)
        ),
      ]);
 
      // Close database connections
      await db.destroy();
      await redis.quit();
 
      console.log('Graceful shutdown completed');
      process.exit(0);
    } catch (error) {
      console.error('Error during shutdown:', error);
      process.exit(1);
    }
  });
 
  // Force shutdown after timeout
  setTimeout(() => {
    console.error('Forced shutdown after timeout');
    process.exit(1);
  }, 35000);
}
 
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

Structured Logging

Replace console.log with structured logging that supports log aggregation and querying:

const pino = require('pino');
 
const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level(label) {
      return { level: label };
    },
  },
  serializers: {
    err: pino.stdSerializers.err,
    req: pino.stdSerializers.req,
    res: pino.stdSerializers.res,
  },
  redact: {
    paths: ['req.headers.authorization', 'req.headers.cookie'],
    remove: true,
  },
});
 
// Request logging middleware
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    logger.info({
      req,
      res,
      responseTime: Date.now() - start,
    }, `${req.method} ${req.url} ${res.statusCode}`);
  });
  next();
});

Rate Limiting and Abuse Prevention

Protect your API endpoints with rate limiting that adapts to different client types:

const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
 
const apiLimiter = rateLimit({
  store: new RedisStore({ client: redisClient }),
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window
  standardHeaders: true,
  legacyHeaders: false,
  keyGenerator: (req) => req.user?.id || req.ip,
  handler: (req, res) => {
    logger.warn({ ip: req.ip, user: req.user?.id }, 'Rate limit exceeded');
    res.status(429).json({
      error: 'Too many requests',
      retryAfter: Math.ceil(req.rateLimit.resetTime / 1000),
    });
  },
});
 
app.use('/api/', apiLimiter);

These operational practices form the foundation of a reliable production service that can handle real-world traffic patterns and failure scenarios.

Community Resources and Further Learning

The technology landscape evolves rapidly, making continuous learning essential for maintaining expertise. Building a systematic approach to staying current with developments in your technology stack ensures you can leverage new features and avoid deprecated patterns.

Curated Learning Pathways

Rather than consuming content randomly, create structured learning pathways aligned with your current projects and career goals. Start with official documentation and specification documents, which provide the most accurate and comprehensive information. Follow this with hands-on tutorials and workshops that reinforce concepts through practical application.

Technical blogs from framework maintainers and core team members often provide deeper insights into design decisions and upcoming features. Subscribe to the official blogs of your primary frameworks and libraries to stay ahead of breaking changes and deprecation timelines.

Contributing to Open Source

Contributing to open-source projects in your technology stack provides unparalleled learning opportunities. Start with documentation improvements and bug reports, then progress to fixing small issues tagged as "good first issue" in your favorite projects. This direct engagement with maintainers and the codebase accelerates your understanding far beyond what passive learning can achieve.

# Setting up for contribution
git clone https://github.com/project/repository.git
cd repository
git checkout -b fix/issue-description
 
# Run the project's contribution setup
npm run setup:dev
npm run test  # Ensure tests pass before making changes
 
# Make your changes, then run the full test suite
npm run test:full
npm run lint
npm run build
 
# Submit your contribution
git add -A
git commit -m "fix: description of the fix
 
Closes #1234"
git push origin fix/issue-description

Building a Technical Knowledge Base

Maintain a personal knowledge base that captures insights, solutions, and patterns you discover during your work. Tools like Obsidian, Notion, or even a simple Markdown repository can serve as an external memory that grows more valuable over time.

Organize your notes by topic rather than chronologically, and include code examples, links to relevant documentation, and explanations of why certain approaches work better than others. When you encounter a particularly insightful article or conference talk, write a summary that captures the key takeaways and how they apply to your current projects.

Follow key conferences and their published talks to stay informed about emerging patterns and best practices. Many conferences publish recorded talks on YouTube within weeks of the event, making world-class technical content freely accessible.

Join relevant Discord servers, Slack communities, and forums where practitioners discuss real-world challenges and solutions. These communities provide early warning about emerging issues and access to collective wisdom that isn't available through formal documentation.

Mentorship and Knowledge Sharing

Teaching others is one of the most effective ways to deepen your own understanding. Consider writing technical blog posts, giving talks at local meetups, or mentoring junior developers. The process of explaining concepts to others forces you to organize your knowledge and identify gaps in your understanding.

Pair programming sessions with colleagues of different experience levels create mutual learning opportunities. Senior developers gain fresh perspectives on problems they've solved the same way for years, while junior developers benefit from exposure to production-grade thinking and decision-making processes.

Conclusion

Deno 2.0 is now a production-ready alternative to Node.js. Key takeaways:

  1. Full npm compatibility means zero migration cost for existing packages
  2. Native TypeScript execution eliminates build configuration and speeds up development
  3. Built-in toolchain (fmt, lint, test, bench) reduces dependency sprawl
  4. Fresh framework with islands architecture delivers excellent performance for server-rendered apps
  5. Permission-based security model is a meaningful improvement over Node.js's unrestricted access

Start by trying Deno 2.0 for your next greenfield project. The development experience is genuinely better—native TypeScript, built-in tools, and zero configuration. For existing Node.js projects, evaluate whether the security model and edge deployment capabilities justify the migration effort.