Deno
Deno is a secure TypeScript/JavaScript runtime with built-in tooling. No node_modules, no bundler config, no tsconfig.json needed. TypeScript runs natively. All APIs use web standards (fetch, Request, Response, Web Streams).
CLI Commands
deno run server.ts # Run a file
deno run --watch server.ts # Restart on file changes
deno task dev # Run task from deno.json
deno test # Run tests
deno bench # Run benchmarks
deno fmt # Format code
deno lint # Lint code
deno check server.ts # Type-check without running
deno compile --output myapp server.ts # Compile to standalone binary
deno serve server.ts # Run an HTTP server (export default handler)
deno doc mod.ts # Generate documentation
deno info server.ts # Show dependency tree
deno repl # Interactive REPL
deno upgrade # Upgrade Deno itself
Permissions Model
Deno is secure by default. Scripts have no file, network, or environment access unless explicitly granted.
deno run --allow-read server.ts # All file reads
deno run --allow-read=/tmp,./data server.ts # Specific paths only
deno run --allow-write=./output server.ts # Write to specific dir
deno run --allow-net server.ts # All network access
deno run --allow-net=api.example.com server.ts # Specific hosts
deno run --allow-env=DATABASE_URL,PORT server.ts # Specific env vars
deno run --allow-run=git,deno server.ts # Specific subprocesses
deno run --allow-ffi server.ts # Foreign function interface
deno run --allow-sys server.ts # System info (OS, memory)
deno run -A server.ts # Allow all (development only)
deno run --deny-net server.ts # Explicitly deny network
Prompt mode: without flags, Deno asks interactively at runtime. Use --no-prompt to deny all unpermitted access silently.
deno.json Configuration
{
"tasks": {
"dev": "deno run --watch --allow-net --allow-read server.ts",
"test": "deno test --allow-read",
"build": "deno compile --output dist/app server.ts"
},
"imports": {
"@std/": "jsr:@std/",
"oak": "jsr:@oak/oak@^17",
"zod": "npm:zod@^3.23"
},
"compilerOptions": {
"strict": true,
"jsx": "react-jsx",
"jsxImportSource": "preact"
},
"fmt": {
"useTabs": false,
"lineWidth": 100,
"indentWidth": 2,
"semiColons": true,
"singleQuote": false
},
"lint": {
"rules": {
"exclude": ["no-unused-vars"]
}
},
"exclude": ["node_modules", "dist"],
"lock": true,
"nodeModulesDir": "auto"
}
The imports field replaces import maps. All bare specifiers resolve through it. Run deno add @std/path to auto-add entries.
Standard Library (@std)
All modules live on JSR under @std. Add with deno add @std/<module>.
// File I/O
import { exists, ensureDir, copy, walk } from "@std/fs";
import { join, resolve, basename, extname } from "@std/path";
await ensureDir("./output");
for await (const entry of walk("./src", { exts: [".ts"] })) {
console.log(entry.path);
}
// HTTP
import { serveDir } from "@std/http/file-server";
// Testing assertions
import { assertEquals, assertThrows, assertRejects } from "@std/assert";
// Text encoding/decoding
import { encodeBase64, decodeBase64 } from "@std/encoding/base64";
// Datetime
import { format, parse } from "@std/datetime";
// Collections
import { groupBy, partition, sortBy } from "@std/collections";
// Async utilities
import { delay, deadline, retry } from "@std/async";
await retry(() => fetch("https://api.example.com"), { maxAttempts: 3 });
// Streams
import { TextLineStream } from "@std/streams";
npm and Node.js Compatibility
Use npm: specifiers to import any npm package. No install step needed.
import express from "npm:express@4";
import chalk from "npm:chalk@5";
import { z } from "npm:zod@3";
const app = express();
app.get("/", (_req, res) => res.json({ hello: "world" }));
app.listen(3000);
Node built-in modules work with node: prefix:
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { createServer } from "node:http";
import { EventEmitter } from "node:events";
Set "nodeModulesDir": "auto" in deno.json for packages that need node_modules on disk (native addons, frameworks expecting it).
JSR (JavaScript Registry)
JSR is a TypeScript-first registry. Packages work in Deno, Node, and Bun.
deno add @std/path # Add to deno.json imports
deno add @oak/oak # Add third-party package
Publishing to JSR:
// deno.json
{
"name": "@myorg/mylib",
"version": "1.0.0",
"exports": "./mod.ts"
}
deno publish # Publish to JSR
deno publish --dry-run # Preview what would be published
JSR requires explicit type exports. No any in public API signatures.
HTTP Server
Deno.serve (recommended)
Deno.serve({ port: 8000 }, async (req: Request): Promise<Response> => {
const url = new URL(req.url);
if (url.pathname === "/api/health") {
return Response.json({ status: "ok" });
}
if (url.pathname === "/api/users" && req.method === "POST") {
const body = await req.json();
return Response.json({ created: body }, { status: 201 });
}
if (url.pathname.startsWith("/static/")) {
const { serveDir } = await import("@std/http/file-server");
return serveDir(req, { fsRoot: "./public", urlRoot: "static" });
}
return new Response("Not Found", { status: 404 });
});
deno serve (multi-worker)
Export a default object with a fetch handler. Run with deno serve:
// server.ts
export default {
fetch(req: Request): Response {
return new Response("Hello from Deno!");
},
};
deno serve --parallel server.ts # Multi-core, auto-selects worker count
deno serve --port 3000 server.ts
Middleware Pattern
type Handler = (req: Request) => Response | Promise<Response>;
type Middleware = (next: Handler) => Handler;
const logger: Middleware = (next) => async (req) => {
const start = performance.now();
const res = await next(req);
console.log(`${req.method} ${new URL(req.url).pathname} ${res.status} ${(performance.now() - start).toFixed(1)}ms`);
return res;
};
const cors: Middleware = (next) => async (req) => {
const res = await next(req);
res.headers.set("Access-Control-Allow-Origin", "*");
return res;
};
const compose = (...mws: Middleware[]) => (handler: Handler) =>
mws.reduceRight((h, mw) => mw(h), handler);
const app = compose(logger, cors)((req) => Response.json({ ok: true }));
Deno.serve(app);
Deno KV
Built-in key-value store. Works locally (SQLite-backed) and on Deno Deploy (globally distributed).
const kv = await Deno.openKv(); // Local or Deploy
// const kv = await Deno.openKv("https://api.deno.com/databases/<id>/connect"); // Remote
// CRUD
await kv.set(["users", "alice"], { name: "Alice", role: "admin" });
const entry = await kv.get(["users", "alice"]);
console.log(entry.value); // { name: "Alice", role: "admin" }
console.log(entry.versionstamp);
await kv.delete(["users", "alice"]);
// List by prefix
const iter = kv.list({ prefix: ["users"] });
for await (const entry of iter) {
console.log(entry.key, entry.value);
}
// Atomic operations (optimistic concurrency)
const user = await kv.get(["users", "alice"]);
const result = await kv.atomic()
.check(user) // Fail if versionstamp changed
.set(["users", "alice"], { ...user.value, role: "superadmin" })
.sum(["stats", "updates"], 1n) // Atomic counter
.commit();
console.log(result.ok); // true or false
// Enqueue (built-in queue)
await kv.enqueue({ type: "email", to: "alice@example.com", subject: "Welcome" });
kv.listenQueue(async (msg) => {
console.log("Processing:", msg);
});
// Watch for changes
const stream = kv.watch([["users", "alice"]]);
for await (const [entry] of stream) {
console.log("Changed:", entry.value);
}
Deno Deploy
Serverless edge platform. Runs Deno code on 35+ regions. Supports Deno KV, BroadcastChannel, cron.
# Install deployctl
deno install -Arf jsr:@deno/deployctl
# Deploy
deployctl deploy --project=my-app server.ts
deployctl deploy --project=my-app --prod server.ts # Production deploy
# From GitHub: connect repo in Deno Deploy dashboard for automatic deploys
Deno Deploy supports:
Deno.servefor HTTPDeno.openKv()for globally distributed KVDeno.cron()for scheduled tasksBroadcastChannelfor cross-isolate communication- Web standard APIs (fetch, crypto, streams)
- npm packages via
npm:specifiers
// Scheduled tasks on Deploy
Deno.cron("cleanup", "0 * * * *", async () => {
const kv = await Deno.openKv();
// Cleanup logic
});
Fresh Framework
Full-stack web framework for Deno. Islands architecture: zero JS shipped by default, interactive components hydrated on demand.
deno run -A -r https://fresh.deno.dev my-app # Scaffold project
cd my-app && deno task dev # Start dev server
Routes (file-based)
routes/
index.tsx -> GET /
about.tsx -> GET /about
blog/[slug].tsx -> GET /blog/:slug
api/users.ts -> API route (no UI)
_layout.tsx -> Layout wrapper
_404.tsx -> Custom 404
Route with Handler
// routes/api/users.ts
import { Handlers } from "$fresh/server.ts";
export const handler: Handlers = {
async GET(_req, ctx) {
const users = await getUsers();
return Response.json(users);
},
async POST(req, _ctx) {
const body = await req.json();
const user = await createUser(body);
return Response.json(user, { status: 201 });
},
};
Island Component
// islands/Counter.tsx
import { useSignal } from "@preact/signals";
export default function Counter() {
const count = useSignal(0);
return (
<div>
<p>{count.value}</p>
<button => count.value++}>+1</button>
</div>
);
}
// Use in a route (only Counter hydrates on client)
// routes/index.tsx
import Counter from "../islands/Counter.tsx";
export default function Home() {
return <div><h1>Welcome</h1><Counter /></div>;
}
Testing
import { assertEquals, assertRejects, assertThrows } from "@std/assert";
import { describe, it, beforeEach, afterEach } from "@std/testing/bdd";
import { stub, spy, assertSpyCalls } from "@std/testing/mock";
import { FakeTime } from "@std/testing/time";
// Basic
Deno.test("addition", () => {
assertEquals(1 + 1, 2);
});
// Async
Deno.test("fetch data", async () => {
const res = await fetch("https://api.example.com/data");
assertEquals(res.status, 200);
});
// BDD style
describe("UserService", () => {
let service: UserService;
beforeEach(() => { service = new UserService(); });
it("should create a user", async () => {
const user = await service.create({ name: "Alice" });
assertEquals(user.name, "Alice");
});
});
// Mocking
Deno.test("stubbing", () => {
const fn = spy(() => 42);
fn();
assertSpyCalls(fn, 1);
using _stub = stub(Math, "random", () => 0.5);
assertEquals(Math.random(), 0.5);
// Stub auto-restores when _stub goes out of scope (using declaration)
});
// Fake time
Deno.test("fake time", () => {
using time = new FakeTime();
const start = Date.now();
time.tick(1000);
assertEquals(Date.now() - start, 1000);
});
deno test # Run all tests
deno test tests/auth_test.ts # Specific file
deno test --filter "UserService" # Filter by name
deno test --coverage=cov_profile # Collect coverage
deno coverage cov_profile # Display coverage report
deno coverage cov_profile --lcov > lcov.info # LCOV output
deno test --doc # Test code examples in JSDoc
FFI (Foreign Function Interface)
Call native C/Rust libraries from Deno.
const lib = Deno.dlopen("./libmath.so", {
add: { parameters: ["i32", "i32"], result: "i32" },
multiply: { parameters: ["f64", "f64"], result: "f64" },
});
console.log(lib.symbols.add(2, 3)); // 5
console.log(lib.symbols.multiply(2.5, 4)); // 10.0
lib.close();
Requires --allow-ffi (or -A). Supported types: i8, i16, i32, i64, u8, u16, u32, u64, f32, f64, pointer, buffer, void.
WebSocket and WebWorker
WebSocket Server
Deno.serve((req) => {
if (req.headers.get("upgrade") === "websocket") {
const { socket, response } = Deno.upgradeWebSocket(req);
socket.onopen = () => console.log("connected");
socket.onmessage = (e) => socket.send(`echo: ${e.data}`);
socket.onclose = () => console.log("disconnected");
return response;
}
return new Response("Not a WebSocket request", { status: 400 });
});
Web Workers
// worker.ts
self.onmessage = (e: MessageEvent) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
// main.ts
const worker = new Worker(new URL("./worker.ts", import.meta.url).href, {
type: "module",
});
worker.postMessage({ input: "data" });
worker.onmessage = (e) => console.log("Result:", e.data);
Migration from Node.js
- Rename
package.jsonscripts todeno.jsontasks - Replace bare imports with
npm:specifiers or JSR packages - Add
node:prefix to Node built-in imports (fs->node:fs) - Add permission flags to run commands
- Replace
__dirnamewithimport.meta.dirname(Deno 1.40+) - Replace
__filenamewithimport.meta.filename - Replace
require()withimport(Deno supports CJS vianpm:but prefers ESM) - Set
"nodeModulesDir": "auto"if packages expectnode_modules
# Before (Node)
node --env-file=.env server.js
# After (Deno)
deno run --allow-net --allow-read --allow-env server.ts
Deno vs Node vs Bun
| Feature | Deno | Node | Bun |
|---|---|---|---|
| TypeScript | Native, zero config | Requires transpiler | Native |
| Security | Permissions by default | No sandbox | No sandbox |
| Package manager | URL imports + deno add |
npm/yarn/pnpm | bun install |
| Registry | JSR + npm | npm | npm |
| Formatter | deno fmt built-in |
Prettier (external) | None built-in |
| Linter | deno lint built-in |
ESLint (external) | None built-in |
| Test runner | deno test built-in |
Node test / Jest | bun test built-in |
| Config files | deno.json only |
package.json + many | package.json + bunfig.toml |
| Web standards | First-class | Polyfilled | Partial |
| Edge deploy | Deno Deploy | Various | None native |
| KV store | Deno KV built-in | External (Redis etc.) | External |
| Compile to binary | deno compile |
pkg/nexe (third-party) | bun build --compile |