Deno 2.0 Complete Guide — TypeScript Runtime with Node.js Compatibility
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.tsModern 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"
}
}Hono with Deno (Recommended)
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-allSecurity 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.tsCommon Mistakes
- Using Node.js-style
require()in Deno — use ES moduleimportsyntax - Forgetting permission flags — Deno throws permission errors at runtime, not during build
- Using
jsr:@std/pathinstead ofnpm:path— both work but JSR is preferred for Deno-native code - Running
deno runwithout--watchin development — you lose hot reload - Not specifying version constraints in
deno.jsonimports — unpinned imports can break on updates
Best Practices
- Use
deno.jsonimportsmap to centralize and version all dependencies - Specify minimal permissions per environment — avoid
--allow-allin production - Use Deno KV for simple persistent storage on Deno Deploy without managing a separate database
- Use
deno compileto produce standalone executables for containerless deployment - Write tests in
_test.tsfiles alongside source files —deno testdiscovers 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 StandardRequest/ResponseAPI — 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, anddeno testare built-in — no configuration files needed- TypeScript is native in Deno — no
tsc,tsx, orts-noderequired deno compilebundles 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