Deno 2.0: The Modern JavaScript Runtime
Deno 2.0 represents a significant maturation: full Node.js and npm compatibility while maintaining its security-first, standards-based design. It is now a realistic alternative to Node.js for new projects.
What Makes Deno Different
| Feature | Node.js | Deno 2.0 |
|---|---|---|
| TypeScript | Requires ts-node/esbuild | Native, zero config |
| Security | Full system access | Deny by default |
| Package manager | npm/yarn/pnpm | npm + jsr: imports |
| Tooling | Separate tools needed | Built-in (fmt, lint, test, bench) |
| Standards | Node-specific APIs | Web Platform APIs |
| Node compat | Native | 95%+ via node: prefix |
Getting Started
curl -fsSL https://deno.land/install.sh | sh
# Run TypeScript directly - no compilation step
deno run main.ts
# With permissions
deno run --allow-net --allow-read main.ts
# REPL
deno
Security Model
Deno denies all system access by default:
// This will FAIL without --allow-net
const response = await fetch('https://api.example.com/data');
// This will FAIL without --allow-read
const text = await Deno.readTextFile('./config.json');
// Granular permissions
// --allow-net=api.example.com (only this host)
// --allow-read=/app/config (only this path)
// --allow-env=DATABASE_URL (only this env var)
// deno.json - production permissions
// {
// 'tasks': {
// 'start': 'deno run --allow-net=:8000 --allow-env=DATABASE_URL --allow-read=./static main.ts'
// }
// }
Modern HTTP Server
// server.ts - no dependencies needed!
import { serveFile } from 'jsr:@std/http/file-server';
const handler = async (req: Request): Promise<Response> => {
const url = new URL(req.url);
if (url.pathname === '/api/users') {
const users = await getUsers();
return Response.json(users);
}
if (url.pathname.startsWith('/static/')) {
return serveFile(req, '.' + url.pathname);
}
return new Response('Not Found', { status: 404 });
};
Deno.serve({ port: 8000, hostname: '0.0.0.0' }, handler);
console.log('Listening on http://localhost:8000');
Using npm Packages
Deno 2.0 supports npm packages natively:
// Use npm packages directly
import express from 'npm:express@4';
import { z } from 'npm:zod';
import Stripe from 'npm:stripe';
// Or with deno.json imports map
// deno.json:
// {
// 'imports': {
// 'express': 'npm:express@4',
// 'zod': 'npm:zod@3'
// }
// }
import express from 'express';
import { z } from 'zod';
const app = express();
const UserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().min(0).max(150)
});
app.post('/users', async (req, res) => {
const result = UserSchema.safeParse(req.body);
if (!result.success) return res.status(400).json(result.error);
// ...
});
Built-in Toolchain
# Format (replaces prettier)
deno fmt
deno fmt --check # CI check
# Lint (replaces eslint for common rules)
deno lint
# Test (replaces jest/mocha)
deno test
deno test --coverage
deno coverage --html # HTML coverage report
# Benchmark
deno bench
# Type check only
deno check main.ts
# Bundle for browser
deno bundle main.ts output.js
# Compile to single executable
deno compile --allow-net --allow-read main.ts
Testing
// user.test.ts
import { assertEquals, assertRejects } from 'jsr:@std/assert';
import { getUser, createUser } from './user.ts';
Deno.test('getUser returns user by id', async () => {
const user = await getUser('123');
assertEquals(user.id, '123');
assertEquals(user.name, 'Alice');
});
Deno.test('createUser rejects invalid email', async () => {
await assertRejects(
() => createUser({ name: 'Bob', email: 'not-an-email' }),
Error,
'Invalid email'
);
});
// Grouped tests with setup/teardown
Deno.test('user service', async (t) => {
const db = await createTestDatabase();
await t.step('creates user', async () => {
const user = await createUser(db, { name: 'Test' });
assertEquals(user.name, 'Test');
});
await t.step('lists users', async () => {
const users = await listUsers(db);
assertEquals(users.length, 1);
});
await db.close();
});
Migrating from Node.js
// Node.js - before
// const fs = require('fs/promises');
// const path = require('path');
// Deno - after (option 1: node: prefix)
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
const text = await readFile(join('./data', 'file.txt'), 'utf-8');
// Deno - after (option 2: native Deno APIs)
const text2 = await Deno.readTextFile('./data/file.txt');
// Environment variables
// Node: process.env.DATABASE_URL
// Deno: Deno.env.get('DATABASE_URL')
// Process exit
// Node: process.exit(1)
// Deno: Deno.exit(1)
Deploy with Deno Deploy
// Runs globally on Deno Deploy's edge network (35+ regions)
// No configuration needed for basic deployments
// deploy.ts - uploaded directly
const kv = await Deno.openKv(); // Built-in distributed KV store
Deno.serve(async (req: Request) => {
const url = new URL(req.url);
if (req.method === 'POST' && url.pathname === '/count') {
const key = ['visits', url.hostname];
const current = (await kv.get<number>(key)).value ?? 0;
await kv.set(key, current + 1);
return Response.json({ count: current + 1 });
}
return new Response('Hello from edge!', {
headers: { 'Content-Type': 'text/plain' }
});
});
Should You Migrate?
Migrate to Deno for new projects when:
- TypeScript is required (zero-config is a real advantage)
- Security model matters (microservices, processing user uploads)
- You want fewer dev dependencies (no eslint, prettier, ts-node, nodemon)
Stay with Node.js when:
- Existing large codebase with deep npm ecosystem dependencies
- Team expertise and existing tooling is heavily Node-based
- You need native addons (.node files)
Deno 2.0 is production-ready. Its standards-based approach means your code works in browsers and edge runtimes without modification.