Deno 2.0 Complete Guide — TypeScript Runtime with Node.js Compatibility

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

Introduction

Why This Matters

Deno 2.0, released in October 2024, is a significant step change from Deno 1.x. It brings full Node.js and npm compatibility — meaning the same packages that work in Node.js work in Deno. Combined with its security model, native TypeScript support, and modern standard library, Deno 2.0 is a compelling production alternative to Node.js.

The key difference from Node.js: Deno requires explicit permissions to access the filesystem, network, and environment variables. This security model catches accidental data exposure and supply chain attacks at runtime — something Node.js has no equivalent of.

Deno also ships with a complete toolchain: deno fmt, deno lint, deno test, and deno compile — no configuration files required. For TypeScript developers, Deno eliminates the entire build tooling setup that Node.js requires.

Installation and Setup

# Install Deno
curl -fsSL https://deno.land/install.sh | sh
 
# Verify
deno --version
 
# Run TypeScript directly
deno run src/main.ts
 
# With permissions
deno run --allow-net --allow-env --allow-read src/main.ts
 
# All permissions (development only)
deno run --allow-all src/main.ts

Modern HTTP Server with Deno 2.0

Deno 2.0 uses the Web Standard Deno.serve() API.

// src/main.ts
Deno.serve(
  { port: 3000 },
  async (req: Request): Promise<Response> => {
    const url = new URL(req.url);
 
    if (url.pathname === '/health') {
      return Response.json({ status: 'ok', version: '2.0' });
    }
 
    if (url.pathname === '/api/users' && req.method === 'GET') {
      const users = [{ id: '1', name: 'Alice', email: 'alice@example.com' }];
      return Response.json({ users });
    }
 
    if (url.pathname === '/api/users' && req.method === 'POST') {
      const body = await req.json() as { name: string; email: string };
      return Response.json({ id: crypto.randomUUID(), ...body }, { status: 201 });
    }
 
    return new Response('Not Found', { status: 404 });
  }
);
 
console.log('Deno server running on http://localhost:3000');

Using npm Packages in Deno 2.0

Deno 2.0 supports npm packages natively with the npm: specifier.

// Use npm packages directly — no install step needed
import express from 'npm:express@4';
import { z } from 'npm:zod@3';
import { Hono } from 'npm:hono@4';
 
// Or use JSR (Deno's package registry)
import { Router } from 'jsr:@std/http/router';
 
// Or add to deno.json for shorter imports
// deno.json
{
  "imports": {
    "hono": "npm:hono@4",
    "zod": "npm:zod@3",
    "@std/http": "jsr:@std/http@^1.0.0"
  },
  "tasks": {
    "dev": "deno run --allow-net --allow-env --watch src/main.ts",
    "start": "deno run --allow-net --allow-env src/main.ts",
    "test": "deno test --allow-all",
    "lint": "deno lint",
    "fmt": "deno fmt"
  }
}
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
 
const app = new Hono();
 
app.use('*', logger());
app.use('/api/*', cors());
 
const createUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
});
 
app.post(
  '/api/users',
  zValidator('json', createUserSchema),
  async (c) => {
    const { name, email } = c.req.valid('json');
    const user = { id: crypto.randomUUID(), name, email };
    return c.json(user, 201);
  }
);
 
app.get('/api/users', (c) => {
  return c.json({ users: [], total: 0 });
});
 
Deno.serve({ port: 3000 }, app.fetch);

Deno KV — Built-in Key-Value Database

Deno KV is a built-in, globally consistent key-value store that works locally and on Deno Deploy.

// No configuration, no connection string
const kv = await Deno.openKv();
 
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: string;
}
 
// Set a value
async function createUser(data: Omit<User, 'id' | 'createdAt'>): Promise<User> {
  const user: User = {
    id: crypto.randomUUID(),
    ...data,
    createdAt: new Date().toISOString(),
  };
 
  await kv.set(['users', user.id], user);
  await kv.set(['users_by_email', user.email], user.id);
 
  return user;
}
 
// Get a value
async function getUserById(id: string): Promise<User | null> {
  const entry = await kv.get<User>(['users', id]);
  return entry.value;
}
 
// List values by prefix
async function listUsers(): Promise<User[]> {
  const users: User[] = [];
  for await (const entry of kv.list<User>({ prefix: ['users'] })) {
    if (entry.value) users.push(entry.value);
  }
  return users;
}
 
// Atomic transactions
async function transferData(fromId: string, toId: string): Promise<boolean> {
  const from = await kv.get<User>(['users', fromId]);
  const to = await kv.get<User>(['users', toId]);
 
  if (!from.value || !to.value) return false;
 
  const result = await kv.atomic()
    .check(from)
    .check(to)
    .set(['users', fromId], { ...from.value, updatedAt: new Date().toISOString() })
    .set(['users', toId], { ...to.value, updatedAt: new Date().toISOString() })
    .commit();
 
  return result.ok;
}

Built-in Testing

// src/users_test.ts
import { assertEquals, assertRejects } from 'jsr:@std/assert';
 
Deno.test('getUserById returns null for missing user', async () => {
  const user = await getUserById('non-existent-id');
  assertEquals(user, null);
});
 
Deno.test('createUser returns a valid user', async () => {
  const user = await createUser({ name: 'Alice', email: 'alice@example.com' });
  assertEquals(user.name, 'Alice');
  assertEquals(typeof user.id, 'string');
});
 
// Run: deno test --allow-all

Security Permissions

Deno's permission model is one of its most important features for production security.

# Minimal permissions for a typical API
deno run \
  --allow-net=localhost:3000,api.stripe.com \
  --allow-env=DATABASE_URL,JWT_SECRET,PORT \
  --allow-read=/app/static \
  src/main.ts
 
# Compile to executable with embedded permissions
deno compile \
  --allow-net \
  --allow-env \
  --output dist/api \
  src/main.ts

Common Mistakes

  • Using Node.js-style require() in Deno — use ES module import syntax
  • Forgetting permission flags — Deno throws permission errors at runtime, not during build
  • Using jsr:@std/path instead of npm:path — both work but JSR is preferred for Deno-native code
  • Running deno run without --watch in development — you lose hot reload
  • Not specifying version constraints in deno.json imports — unpinned imports can break on updates

Best Practices

  • Use deno.json imports map to centralize and version all dependencies
  • Specify minimal permissions per environment — avoid --allow-all in production
  • Use Deno KV for simple persistent storage on Deno Deploy without managing a separate database
  • Use deno compile to produce standalone executables for containerless deployment
  • Write tests in _test.ts files alongside source files — deno test discovers them automatically

Key Takeaways

  • Deno 2.0 supports npm packages natively with npm: specifiers — most Node.js packages work directly
  • Deno.serve() uses the Web Standard Request/Response API — identical to Cloudflare Workers and Bun
  • Deno KV is a built-in key-value database with global consistency on Deno Deploy
  • Deno's permission model (--allow-net, --allow-env) provides runtime security boundaries
  • deno fmt, deno lint, and deno test are built-in — no configuration files needed
  • TypeScript is native in Deno — no tsc, tsx, or ts-node required
  • deno compile bundles TypeScript apps into single-file executables for easy deployment
  • JSR (jsr.io) is Deno's typed package registry with stricter quality requirements than npm

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading