Deno 2.0: The TypeScript Runtime That Finally Makes Sense



Deno 1.0 was interesting. Deno 2.0 is practical. After years of refinement, Deno has evolved from “cool experiment” to “legitimate Node.js alternative.” Here’s what changed and why it matters.

Code on screen Photo by Florian Olivo on Unsplash

What’s New in Deno 2.0

1. Full Node.js Compatibility

The biggest barrier to Deno adoption was npm packages. Deno 2.0 solves this:

// Just import npm packages directly
import express from "npm:express@4";
import { PrismaClient } from "npm:@prisma/client";

const app = express();
const prisma = new PrismaClient();

app.get("/users", async (req, res) => {
  const users = await prisma.user.findMany();
  res.json(users);
});

app.listen(3000);

No package.json. No node_modules. It just works.

2. Native TypeScript (Still)

TypeScript without compilation. This was always Deno’s superpower:

// main.ts - run directly with `deno run main.ts`
interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`https://api.example.com/users/${id}`);
  return response.json();
}

const user = await fetchUser(1);
console.log(`Hello, ${user.name}!`);

3. Built-in Tooling

Everything you need, built-in:

# Format code
deno fmt

# Lint code
deno lint

# Run tests
deno test

# Generate documentation
deno doc

# Bundle for browser
deno bundle

# Check types
deno check

# Compile to binary
deno compile --output myapp main.ts

4. Permission System

Security by default. No more “hope this npm package doesn’t steal my env vars”:

# Explicit permissions
deno run --allow-net --allow-read=./data main.ts

# Or grant all (not recommended for production)
deno run --allow-all main.ts
// Request permissions at runtime
const status = await Deno.permissions.request({ name: "env" });
if (status.state === "granted") {
  console.log(Deno.env.get("API_KEY"));
}

Building a Real API with Deno 2.0

Let’s build a production-ready API:

// api/main.ts
import { Application, Router } from "https://deno.land/x/oak@v12.6.1/mod.ts";
import { z } from "npm:zod";

// Validation schemas
const UserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});

// In-memory store (use a real DB in production)
const users = new Map<string, { id: string; name: string; email: string }>();

const router = new Router();

router
  .get("/users", (ctx) => {
    ctx.response.body = Array.from(users.values());
  })
  .get("/users/:id", (ctx) => {
    const user = users.get(ctx.params.id);
    if (!user) {
      ctx.response.status = 404;
      ctx.response.body = { error: "User not found" };
      return;
    }
    ctx.response.body = user;
  })
  .post("/users", async (ctx) => {
    const body = await ctx.request.body().value;
    const result = UserSchema.safeParse(body);
    
    if (!result.success) {
      ctx.response.status = 400;
      ctx.response.body = { errors: result.error.issues };
      return;
    }
    
    const id = crypto.randomUUID();
    const user = { id, ...result.data };
    users.set(id, user);
    
    ctx.response.status = 201;
    ctx.response.body = user;
  });

const app = new Application();

// Logging middleware
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.request.method} ${ctx.request.url} - ${ms}ms`);
});

app.use(router.routes());
app.use(router.allowedMethods());

console.log("Server running on http://localhost:8000");
await app.listen({ port: 8000 });

Developer working Photo by Christopher Gower on Unsplash

Deno 2.0 vs Node.js

Project Structure Comparison

Node.js project:

my-app/
├── package.json
├── package-lock.json
├── node_modules/         # 500MB of dependencies
├── tsconfig.json
├── .eslintrc.js
├── .prettierrc
├── src/
│   └── index.ts
└── dist/                 # Compiled output
    └── index.js

Deno project:

my-app/
├── deno.json            # Optional config
└── main.ts              # That's it

Performance Benchmarks

MetricNode.js 22Deno 2.0
HTTP req/sec45,00052,000
Startup time35ms25ms
Memory (idle)40MB28MB
Cold start120ms80ms

Feature Comparison

FeatureNode.jsDeno 2.0
TypeScriptVia tsc/esbuildNative
Top-level await
ES Modules✅ (default)
npm packagesNativeVia npm: prefix
SecurityNonePermission-based
Built-in tools✅ (fmt, lint, test)
Single binary✅ (deno compile)

Deno Deploy: Serverless Made Simple

Deploy Deno apps globally with zero config:

// main.ts
Deno.serve((req: Request) => {
  return new Response("Hello from the edge!", {
    headers: { "content-type": "text/plain" },
  });
});
# Deploy with one command
deployctl deploy --project=my-app main.ts

That’s it. Your app is running on 35+ edge locations worldwide.

Fresh: Deno’s Full-Stack Framework

Fresh is what Next.js could have been—simpler and faster:

// routes/index.tsx
import { Head } from "$fresh/runtime.ts";
import Counter from "../islands/Counter.tsx";

export default function Home() {
  return (
    <>
      <Head>
        <title>Fresh App</title>
      </Head>
      <div class="p-4 mx-auto max-w-screen-md">
        <h1 class="text-4xl font-bold">Welcome to Fresh</h1>
        <Counter start={3} />
      </div>
    </>
  );
}
// islands/Counter.tsx - Interactive island
import { useState } from "preact/hooks";

interface Props {
  start: number;
}

export default function Counter({ start }: Props) {
  const [count, setCount] = useState(start);
  return (
    <div class="flex gap-2 items-center">
      <button onClick={() => setCount(count - 1)}>-</button>
      <span class="text-xl">{count}</span>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

Key Fresh features:

  • No build step - Files are compiled on-demand
  • Island architecture - Only interactive components ship JS
  • Zero config - Convention over configuration
  • Edge-native - Built for Deno Deploy

Migration from Node.js

Step 1: Update imports

// Before (Node.js)
import express from "express";
import { readFile } from "fs/promises";

// After (Deno)
import express from "npm:express";
// Or use Deno's built-in
const content = await Deno.readTextFile("./data.json");

Step 2: Replace Node APIs

// Node.js
import { createServer } from "http";
import { dirname } from "path";
import { fileURLToPath } from "url";

const __dirname = dirname(fileURLToPath(import.meta.url));

// Deno
import { dirname, fromFileUrl } from "https://deno.land/std/path/mod.ts";

const __dirname = dirname(fromFileUrl(import.meta.url));
// Or just use Deno.cwd() for current directory

Step 3: Add deno.json (optional)

{
  "tasks": {
    "dev": "deno run --watch --allow-all main.ts",
    "start": "deno run --allow-net --allow-read main.ts",
    "test": "deno test --allow-all"
  },
  "imports": {
    "oak": "https://deno.land/x/oak@v12.6.1/mod.ts",
    "zod": "npm:zod@3.22.4"
  }
}

When to Choose Deno 2.0

Choose Deno when:

  • Starting a new TypeScript project
  • Security is a priority
  • You want minimal tooling setup
  • Edge deployment is the target
  • You appreciate built-in formatting/linting

Stick with Node.js when:

  • Existing large Node.js codebase
  • Team is comfortable with Node ecosystem
  • Specific npm packages have issues in Deno
  • You need maximum ecosystem compatibility

Conclusion

Deno 2.0 is no longer an experiment—it’s a production-ready runtime that solves real problems. The full npm compatibility removes the biggest adoption barrier, while keeping the security-first, batteries-included philosophy.

For new projects, especially TypeScript backends and edge applications, Deno 2.0 deserves serious consideration. The developer experience is simply better: less config, faster startup, built-in tools, and real security.

Give it a try. You might not go back.

# Install Deno
curl -fsSL https://deno.land/install.sh | sh

# Create your first app
deno run https://fresh.deno.dev

Have you tried Deno 2.0? What’s your experience? Share in the comments.

이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)