Deno: The Complete Guide for 2026
Deno is a modern JavaScript and TypeScript runtime built on V8 and Rust. Created by Ryan Dahl — the original creator of Node.js — Deno addresses the design mistakes he identified in Node.js: implicit trust of dependencies, lack of built-in TypeScript, no standard library, and cumbersome tooling. With Deno 2.x delivering full npm and Node.js compatibility, there has never been a better time to adopt it.
This guide covers everything from installation to production deployment. Every section includes working code you can copy and run. By the end, you will understand Deno's permission model, TypeScript integration, standard library, HTTP servers, package management, testing, deployment on Deno Deploy, and how to migrate from Node.js.
Table of Contents
- What Is Deno?
- Installation
- Running Scripts and Permissions
- TypeScript First-Class Support
- Standard Library
- HTTP Servers
- Package Management and JSR
- npm Compatibility
- File System Operations
- Testing
- Formatting and Linting
- Task Runner
- Web Standard APIs
- Deno Deploy
- Deno KV
- Fresh Framework
- Migration from Node.js
- Deno vs Node.js vs Bun
- Production Considerations
- Frequently Asked Questions
1. What Is Deno?
Deno is a secure runtime for JavaScript and TypeScript that ships as a single executable with no external dependencies. It differs from Node.js in several fundamental ways:
- Secure by default — no file, network, or environment access unless explicitly granted
- TypeScript built in — run
.tsfiles directly with zero configuration - Web standard APIs —
fetch(),WebSocket,Web Crypto, andStreamswork natively - Built-in tooling — formatter, linter, test runner, coverage, and bundler included
- Standard library — audited, high-quality modules maintained by the Deno team
- npm compatible — Deno 2.x runs most npm packages without modification
Deno 2.x (released October 2024) was a major milestone that added backward compatibility with Node.js built-in APIs and npm packages, letting you adopt Deno incrementally without rewriting your entire codebase.
2. Installation
# macOS / Linux
curl -fsSL https://deno.land/install.sh | sh
# macOS via Homebrew
brew install deno
# Windows (PowerShell)
irm https://deno.land/install.ps1 | iex
# Windows via Scoop or Chocolatey
scoop install deno
choco install deno
# Verify installation
deno --version
# deno 2.x.x, v8 13.x.x, typescript 5.x.x
# Upgrade to latest
deno upgrade
3. Running Scripts and Permissions
The deno run command executes a script. By default, the script runs in a sandbox with no I/O access. You grant permissions with flags:
# Run a local script (no permissions - sandboxed)
deno run main.ts
# Run with specific permissions
deno run --allow-read --allow-net main.ts
# Restrict permissions to specific paths/hosts
deno run --allow-read=/tmp,./data --allow-net=api.example.com main.ts
# Grant all permissions (development only!)
deno run -A main.ts
# Run directly from a URL
deno run https://examples.deno.land/hello-world.ts
Permission Flags Reference
| Flag | Access Granted |
|---|---|
--allow-read | File system reads |
--allow-write | File system writes |
--allow-net | Network access |
--allow-env | Environment variables |
--allow-run | Subprocess execution |
--allow-ffi | Foreign function interface |
--allow-all / -A | All permissions |
4. TypeScript First-Class Support
Deno runs TypeScript natively. No tsconfig.json, no build step, no ts-node. Just write .ts files and run them:
// greeter.ts
interface User {
name: string;
age: number;
}
function greet(user: User): string {
return `Hello, ${user.name}! You are ${user.age} years old.`;
}
const user: User = { name: "Alice", age: 30 };
console.log(greet(user));
// Run: deno run greeter.ts
If you need custom TypeScript settings, add a compilerOptions section to your deno.json:
{
"compilerOptions": {
"strict": true,
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}
5. Standard Library
Deno's standard library (@std) provides audited, well-tested modules published on JSR. Unlike Node.js, where you rely on third-party packages for common tasks, Deno ships official solutions:
import { serveDir } from "@std/http/file-server"; // HTTP file server
import { ensureDir, copy, move } from "@std/fs"; // File system helpers
import { join, basename, extname } from "@std/path"; // Path manipulation
import { assertEquals, assertThrows } from "@std/assert"; // Testing
import { parse as parseCsv } from "@std/csv"; // CSV parsing
import { parse as parseYaml } from "@std/yaml"; // YAML parsing
import { TextLineStream } from "@std/streams"; // Streaming utilities
Add standard library dependencies in your deno.json:
{
"imports": {
"@std/http": "jsr:@std/http@^1.0.0",
"@std/fs": "jsr:@std/fs@^1.0.0",
"@std/path": "jsr:@std/path@^1.0.0",
"@std/assert": "jsr:@std/assert@^1.0.0"
}
}
6. HTTP Servers
Deno.serve() — The Built-in Server
Deno.serve() is the recommended way to create HTTP servers. It is fast, simple, and supports HTTP/2:
Deno.serve({ port: 8000 }, (req: Request): Response => {
const url = new URL(req.url);
if (url.pathname === "/") {
return new Response("Hello from Deno!", {
headers: { "content-type": "text/plain" },
});
}
if (url.pathname === "/api/time") {
return Response.json({ time: new Date().toISOString() });
}
return new Response("Not Found", { status: 404 });
});
Using Hono Framework
For more complex applications, use a framework like Hono which works natively with Deno:
import { Hono } from "npm:hono";
import { cors } from "npm:hono/cors";
import { logger } from "npm:hono/logger";
const app = new Hono();
app.use("*", logger());
app.use("/api/*", cors());
app.get("/", (c) => c.text("Hello Hono on Deno!"));
app.get("/api/users/:id", (c) => {
return c.json({ id: c.req.param("id"), name: "Alice" });
});
app.post("/api/users", async (c) => {
const body = await c.req.json();
return c.json({ id: crypto.randomUUID(), ...body }, 201);
});
Deno.serve({ port: 3000 }, app.fetch);
Other popular frameworks include Oak (jsr:@oak/oak), which provides an Express-like middleware API, and Fresh (covered in section 16), which is a full-stack framework with server-side rendering.
7. Package Management and JSR
Deno uses deno.json as its configuration and package manifest. Import maps let you create clean aliases:
{
"imports": {
"@std/http": "jsr:@std/http@^1.0.0",
"@std/assert": "jsr:@std/assert@^1.0.0",
"hono": "npm:hono@^4.0.0",
"zod": "npm:zod@^3.23.0",
"#/": "./src/"
},
"tasks": {
"dev": "deno run --watch -A src/main.ts",
"start": "deno run -A src/main.ts"
}
}
JSR (JavaScript Registry) is the modern package registry built for Deno and compatible with Node.js. It supports TypeScript natively and generates documentation automatically:
# Add a JSR package
deno add jsr:@std/http
# Add an npm package
deno add npm:express
# Install all dependencies (creates deno.lock)
deno install
Use #/ import aliases to avoid deep relative paths. The mapping "#/": "./src/" lets you write import { db } from "#/lib/database.ts" instead of "../../../lib/database.ts".
8. npm Compatibility
Deno 2.x can use most npm packages directly. There are three ways to import them:
// Method 1: npm: specifier (no install needed)
import express from "npm:express@4";
import { z } from "npm:zod";
// Method 2: Import map in deno.json (recommended for projects)
// deno.json: { "imports": { "express": "npm:express@4" } }
import express from "express";
// Method 3: package.json compatibility
// If you have a package.json, Deno reads it automatically
import chalk from "chalk";
Using Express with Deno
import express from "npm:express@4";
const app = express();
app.use(express.json());
app.get("/api/hello", (req, res) => {
res.json({ message: "Express running on Deno!" });
});
app.listen(3000, () => console.log("Server on http://localhost:3000"));
deno run --allow-net --allow-read --allow-env server.ts
9. File System Operations
Deno provides built-in file system APIs using async/await by default:
// Reading and writing files
const text = await Deno.readTextFile("./config.json");
const config = JSON.parse(text);
await Deno.writeTextFile("./output.txt", "Hello, Deno!");
const bytes = await Deno.readFile("./image.png"); // Uint8Array
// Directory operations
await Deno.mkdir("./data/logs", { recursive: true });
for await (const entry of Deno.readDir("./src")) {
console.log(`${entry.isDirectory ? "[DIR]" : "[FILE]"} ${entry.name}`);
}
// File info, copy, rename, remove
const info = await Deno.stat("./main.ts");
console.log(`Size: ${info.size} bytes, Modified: ${info.mtime}`);
await Deno.copyFile("./a.txt", "./b.txt");
await Deno.rename("./old.txt", "./new.txt");
await Deno.remove("./tmp", { recursive: true });
// Standard library helpers for advanced operations
import { ensureDir, walk } from "@std/fs";
await ensureDir("./cache/images");
for await (const entry of walk("./src", { exts: [".ts"] })) {
console.log(entry.path);
}
10. Testing
Deno has a built-in test runner. No installation, no configuration. Write tests with Deno.test() and use assertions from the standard library:
import { assertEquals, assertThrows, assertRejects } from "@std/assert";
// Basic test
Deno.test("addition works", () => {
assertEquals(2 + 2, 4);
});
// Async test
Deno.test("fetch returns data", async () => {
const res = await fetch("https://api.example.com/health");
assertEquals(res.status, 200);
});
// Test with steps (subtests)
Deno.test("user operations", async (t) => {
let userId: string;
await t.step("create user", async () => {
const user = await createUser("Alice", "alice@test.com");
assertEquals(user.name, "Alice");
userId = user.id;
});
await t.step("fetch user", async () => {
const user = await getUser(userId);
assertEquals(user.name, "Alice");
});
await t.step("delete user", async () => {
await deleteUser(userId);
await assertRejects(() => getUser(userId));
});
});
// Testing exceptions
Deno.test("throws on invalid input", () => {
assertThrows(() => parseConfig(""), Error, "Config cannot be empty");
});
# Run all tests
deno test
# Run with permissions and watch mode
deno test --allow-net --allow-read --watch
# Coverage report
deno test --coverage=./coverage
deno coverage ./coverage --lcov > coverage.lcov
11. Formatting and Linting
Deno includes a formatter and linter out of the box — no Prettier or ESLint needed:
# Format all files (or check without changing)
deno fmt
deno fmt --check
# Lint all files
deno lint
Configure in deno.json:
{
"fmt": {
"useTabs": false,
"lineWidth": 100,
"indentWidth": 2,
"singleQuote": true
},
"lint": {
"rules": {
"include": ["ban-untagged-todo"],
"exclude": ["no-unused-vars"]
}
},
"exclude": ["node_modules/", "dist/", "coverage/"]
}
12. Task Runner
deno task replaces npm scripts. Define tasks in deno.json:
{
"tasks": {
"dev": "deno run --watch --allow-all src/main.ts",
"start": "deno run --allow-net --allow-read --allow-env src/main.ts",
"test": "deno test --allow-all",
"lint": "deno lint && deno fmt --check",
"check": "deno check src/main.ts",
"build": "deno compile --output=server src/main.ts"
}
}
deno task dev # Run a task
deno task # List all tasks
The deno compile command creates a standalone binary that includes the Deno runtime. You can distribute it to machines that do not have Deno installed.
13. Web Standard APIs
Deno implements web standard APIs so code that runs in the browser often runs in Deno without changes:
// fetch - built in, no node-fetch needed
const res = await fetch("https://api.github.com/users/denoland");
const user = await res.json();
// WebSocket client
const ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = () => ws.send("Hello!");
ws.onmessage = (e) => console.log("Received:", e.data);
// Web Crypto API
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv }, key, new TextEncoder().encode("secret")
);
// Streams API - process large files in chunks
const file = await Deno.open("large-file.txt");
const readable = file.readable.pipeThrough(new TextDecoderStream());
for await (const chunk of readable) { console.log(chunk); }
// URLPattern - route matching without a framework
const pattern = new URLPattern({ pathname: "/api/users/:id" });
const match = pattern.exec("https://example.com/api/users/42");
console.log(match?.pathname.groups.id); // "42"
14. Deno Deploy
Deno Deploy is a serverless edge platform that runs your code across 35+ global regions with sub-millisecond cold starts using V8 isolates instead of containers:
// main.ts - a Deno Deploy application
Deno.serve((req: Request) => {
const url = new URL(req.url);
if (url.pathname === "/api/location") {
return Response.json({
message: "Hello from the edge!",
region: Deno.env.get("DENO_REGION") || "local",
timestamp: new Date().toISOString(),
});
}
return new Response("<h1>Welcome</h1>", {
headers: { "content-type": "text/html" },
});
});
# Install deployctl and deploy
deno install -gArf jsr:@deno/deployctl
deployctl deploy --project=my-app src/main.ts
The free tier includes 1 million requests per month, 100 GiB data transfer, and 1 GiB Deno KV storage.
15. Deno KV
Deno KV is a built-in key-value database that works locally (SQLite-backed) and on Deno Deploy (distributed, globally replicated):
const kv = await Deno.openKv();
// Set and get values (keys are arrays for hierarchical namespacing)
await kv.set(["users", "alice"], {
name: "Alice", email: "alice@example.com",
createdAt: new Date().toISOString(),
});
const entry = await kv.get(["users", "alice"]);
console.log(entry.value); // { name: "Alice", ... }
// List values by prefix
for await (const entry of kv.list({ prefix: ["users"] })) {
console.log(entry.key, entry.value);
}
// Atomic transactions with optimistic concurrency
const user = await kv.get(["users", "alice"]);
const result = await kv.atomic()
.check(user) // ensures value hasn't changed since read
.set(["users", "alice"], { ...user.value, verified: true })
.set(["emails", "alice@example.com"], { userId: "alice" })
.commit();
if (!result.ok) console.log("Conflict detected, retry needed");
// Background job queue
await kv.enqueue({ type: "send-email", to: "alice@example.com" });
kv.listenQueue(async (msg) => {
if (msg.type === "send-email") await sendEmail(msg.to);
});
16. Fresh Framework
Fresh is a full-stack web framework for Deno using islands architecture — server-rendered by default with client-side interactivity only where needed:
deno run -A -r https://fresh.deno.dev my-app
cd my-app && deno task start
// routes/index.tsx - server-rendered route (zero JS shipped)
export default function Home() {
return (
<div>
<h1>Welcome to Fresh</h1>
<Counter start={3} />
</div>
);
}
// islands/Counter.tsx - interactive island (hydrated on client)
import { useSignal } from "@preact/signals";
export default function Counter({ start }: { start: number }) {
const count = useSignal(start);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => count.value++}>+1</button>
</div>
);
}
// routes/api/users.ts - API route
import { Handlers } from "$fresh/server.ts";
export const handler: Handlers = {
async GET(_req, _ctx) {
return Response.json(await fetchUsers());
},
async POST(req, _ctx) {
const user = await createUser(await req.json());
return Response.json(user, { status: 201 });
},
};
17. Migration from Node.js
Deno 2.x makes migration practical with Node.js API compatibility. Here is a step-by-step approach:
Step 1: Create deno.json alongside your package.json
{
"nodeModulesDir": "auto",
"tasks": {
"dev": "deno run --watch -A src/index.ts",
"start": "deno run -A src/index.ts"
}
}
Step 2: Gradually replace Node.js APIs with Deno APIs
// Node.js style (still works in Deno 2.x)
import fs from "node:fs/promises";
import path from "node:path";
// Deno equivalent
const data = await Deno.readTextFile("file.txt"); // fs.readFile
const fullPath = new URL("./data", import.meta.url).pathname; // path.join(__dirname)
Step 3: Replace Tooling
| Node.js Tool | Deno Replacement |
|---|---|
| ESLint / Prettier | deno lint / deno fmt |
| Jest / Vitest | deno test |
| ts-node / tsx | deno run (native TS) |
| nodemon | deno run --watch |
| npm scripts / npx | deno task / deno run -A npm:pkg |
18. Deno vs Node.js vs Bun
| Feature | Deno | Node.js | Bun |
|---|---|---|---|
| Engine / Lang | V8 / Rust | V8 / C++ | JSCore / Zig |
| TypeScript | Native | Via transpiler | Native |
| Security | Sandboxed | Full access | Full access |
| npm / Std Lib | Yes (2.x) / Yes | Native / Minimal | Yes / Minimal |
| Built-in Tools | fmt, lint, test | test runner | test, bundler |
| Edge Deploy | Deno Deploy | Various | No official |
Choose Deno for security-first design, built-in TypeScript, comprehensive tooling, or edge deployment. Choose Node.js for maximum ecosystem maturity and existing infrastructure. Choose Bun when raw startup speed is the top priority.
19. Production Considerations
# Compile to standalone binary (no Deno needed on target)
deno compile --output=server --allow-net --allow-read --allow-env src/main.ts
# Cross-compile for Linux
deno compile --target=x86_64-unknown-linux-gnu --output=server src/main.ts
Docker Deployment
FROM denoland/deno:2.1.0
WORKDIR /app
USER deno
COPY deno.json deno.lock ./
RUN deno install
COPY . .
RUN deno cache src/main.ts
EXPOSE 8000
CMD ["run", "--allow-net", "--allow-read", "--allow-env", "src/main.ts"]
Graceful Shutdown
const controller = new AbortController();
const server = Deno.serve(
{ port: 8000, signal: controller.signal },
(req) => {
if (new URL(req.url).pathname === "/health") {
return Response.json({ status: "ok" });
}
return new Response("Hello!");
}
);
Deno.addSignalListener("SIGTERM", () => {
console.log("SIGTERM received, shutting down...");
controller.abort();
});
await server.finished;
Production checklist: use explicit permission flags (never -A), pin dependency versions in deno.lock, run deno check in CI, set up health checks, implement structured logging, and monitor memory usage.
Frequently Asked Questions
What is the difference between Deno and Node.js?
Deno is a secure JavaScript and TypeScript runtime created by Ryan Dahl, who also created Node.js. Key differences include: Deno has built-in TypeScript support with no configuration needed, a permissions system that sandboxes code by default, a built-in standard library, native support for web standard APIs like fetch and Web Streams, built-in tooling (formatter, linter, test runner), and URL-based module imports. Deno 2.x added backward compatibility with npm packages and Node.js APIs, making migration much easier.
Can Deno use npm packages?
Yes. Since Deno 2.x, you can use npm packages directly with the npm: specifier, for example import express from "npm:express". Deno downloads and caches npm packages automatically without needing a node_modules directory (though you can opt into one with --node-modules-dir). Most popular npm packages work out of the box, including Express, React, Prisma, and thousands of others.
How does Deno's permission system work?
Deno runs code in a sandbox by default with no access to the file system, network, environment variables, or subprocess execution. You must explicitly grant permissions using flags: --allow-read, --allow-write, --allow-net, --allow-env, --allow-run, and --allow-ffi. Each flag accepts optional arguments to restrict scope, for example --allow-read=/tmp only allows reading from /tmp. This model prevents supply-chain attacks where a malicious dependency could exfiltrate data.
What is Deno Deploy and how does it work?
Deno Deploy is a serverless edge computing platform that runs your Deno code on V8 isolates distributed across 35+ regions worldwide, providing sub-millisecond cold start times. You deploy via GitHub integration or the deployctl CLI. It supports Deno.serve() for HTTP handlers, Deno KV for distributed storage, BroadcastChannel for cross-isolate communication, and cron jobs. The free tier includes 1 million requests per month.
Should I migrate from Node.js to Deno?
Deno 2.x makes migration practical thanks to npm and Node.js API compatibility. Consider migrating if you want built-in TypeScript, better security through permissions, built-in tooling that replaces ESLint/Prettier/Jest, or edge deployment. You can migrate incrementally using npm: specifiers for existing dependencies and gradually replacing them with Deno-native or JSR packages. For new projects, Deno is an excellent choice. For existing large codebases, evaluate the effort versus benefit based on your specific needs.
Related Resources
- JavaScript Promises & Async/Await Guide — master async patterns used throughout Deno
- Next.js: The Complete Guide — compare Deno Fresh with the React meta-framework
- TypeScript Guide for JavaScript Developers — learn the TypeScript that Deno runs natively
- JS Obfuscator — minify and protect your production JavaScript