Skip to content

Deno 2 Reference: TypeScript, HTTP Server, KV Store, npm Compatibility & Deployment

Deno 2 is TypeScript-first, secure by default, and now ships with Node.js/npm compatibility. The three big differences from Node.js: no node_modules by default (uses a central cache), explicit permission flags for file/network/env access, and first-class TypeScript without a build step. Deno 2 added deno.json workspaces, deno install, and near-complete npm package support.

1. Setup, Running & Permission Model

Install, run TypeScript directly, and understand the permission system
# Install:
curl -fsSL https://deno.land/install.sh | sh

# Run TypeScript directly (no compile step):
deno run main.ts

# Permission flags (deny-all by default — must be explicit):
deno run --allow-net main.ts                    # all network
deno run --allow-net=api.example.com main.ts   # specific host only
deno run --allow-read=/tmp main.ts             # specific path only
deno run --allow-env=DATABASE_URL main.ts      # specific env var only
deno run --allow-all main.ts                   # all permissions (dev only)

# Common flags combination:
deno run \
  --allow-net=api.example.com,db.example.com \
  --allow-read=./data \
  --allow-env=DATABASE_URL,PORT \
  main.ts

# deno.json (replaces package.json + tsconfig.json):
{
  "tasks": {
    "dev": "deno run --watch --allow-net --allow-read main.ts",
    "test": "deno test --allow-net",
    "fmt": "deno fmt",
    "lint": "deno lint"
  },
  "imports": {
    "@std/http": "jsr:@std/http@^1.0.0",
    "zod": "npm:zod@^3.22.0"
  },
  "compilerOptions": {
    "strict": true
  }
}

# Run task:
deno task dev

2. HTTP Server & Hono Framework

Built-in Deno.serve, Hono framework, and request/response handling
// Built-in HTTP server (Deno.serve — no npm package needed):
Deno.serve({ port: 8000 }, async (req: Request) => {
  const url = new URL(req.url);

  if (url.pathname === "/health") {
    return new Response("OK", { status: 200 });
  }

  if (req.method === "POST" && url.pathname === "/api/users") {
    const body = await req.json();
    return Response.json({ id: crypto.randomUUID(), ...body }, { status: 201 });
  }

  return new Response("Not Found", { status: 404 });
});

// Hono (recommended for real apps — runs on Deno, Node, Cloudflare Workers, Bun):
// deno.json: "hono": "npm:hono@^4.0.0"
import { Hono } from "hono";
import { logger } from "hono/logger";
import { jwt } from "hono/jwt";

const app = new Hono();
app.use("*", logger());

// Typed route params:
app.get("/users/:id", async (c) => {
  const id = c.req.param("id");
  const user = await db.getUser(id);
  if (!user) return c.json({ error: "Not found" }, 404);
  return c.json(user);
});

// JWT middleware on a group:
const api = app.basePath("/api");
api.use("*", jwt({ secret: Deno.env.get("JWT_SECRET")! }));
api.get("/me", (c) => {
  const payload = c.get("jwtPayload");
  return c.json({ userId: payload.sub });
});

Deno.serve({ port: 8000 }, app.fetch);

3. File System, KV Store & WebSocket

Deno.readFile/writeFile, Deno.KV (built-in key-value store), and WebSocket server
// File I/O (standard Web APIs + Deno namespace):
const text = await Deno.readTextFile("./data.json");
const data = JSON.parse(text);

await Deno.writeTextFile("./output.json", JSON.stringify(data, null, 2));

// Stream large files:
const file = await Deno.open("./large.csv");
for await (const line of file.readable.pipeThrough(new TextDecoderStream()).pipeThrough(new TransformStream())) {
  // process each chunk
}

// Deno KV (built-in SQLite-backed key-value — no Redis needed for simple cases):
const kv = await Deno.openKv();   // --unstable-kv flag in Deno 1.x, stable in Deno 2

// Set with optional expiration:
await kv.set(["users", userId], { name: "Alice", email: "alice@example.com" });
await kv.set(["sessions", sessionId], { userId }, { expireIn: 86400000 }); // 24h TTL

// Get (returns { key, value, versionstamp } or { value: null }):
const result = await kv.get(["users", userId]);
if (result.value) console.log(result.value.name);

// List by prefix:
const iter = kv.list({ prefix: ["users"] });
for await (const entry of iter) {
  console.log(entry.key, entry.value);
}

// Atomic transaction (compare-and-swap):
const res = await kv.atomic()
  .check({ key: ["counter"], versionstamp: current.versionstamp })
  .set(["counter"], current.value + 1)
  .commit();
if (!res.ok) console.log("conflict, retry");

// WebSocket server:
Deno.serve({ port: 8080 }, (req) => {
  if (req.headers.get("upgrade") !== "websocket") {
    return new Response("Not a WebSocket request", { status: 400 });
  }
  const { socket, response } = Deno.upgradeWebSocket(req);
  socket.onopen = () => socket.send("connected");
  socket.onmessage = (e) => socket.send(`echo: ${e.data}`);
  return response;
});

4. npm Compatibility & Module System

Import npm packages, use JSR registry, and manage dependencies
// Import npm packages directly (Deno 2):
import express from "npm:express@^4";        // npm: specifier
import { z } from "npm:zod@^3.22";
import { PrismaClient } from "npm:@prisma/client";

// JSR (JavaScript Registry — Deno-native, type-safe):
import { assertEquals } from "jsr:@std/assert@^1";
import { parse } from "jsr:@std/csv@^1";

// deno.json imports map (cleaner than inline specifiers):
// "imports": { "zod": "npm:zod@^3.22", "@std/assert": "jsr:@std/assert@^1" }
import { z } from "zod";

// Lock file (generated automatically):
deno cache --lock=deno.lock main.ts   // generate
deno run --lock=deno.lock main.ts     // enforce in CI

// Node.js compatibility (Deno 2 — most npm packages work):
import { readFileSync } from "node:fs";   // node: prefix
import path from "node:path";
import { createServer } from "node:http";

// What still doesn't work: packages that use native addons (.node files),
// some packages that read process.cwd() in ways Deno doesn't support,
// packages that need Node.js-specific globals at module level

5. Testing, Benchmarking & Deployment

Built-in test runner, bench(), Docker, and Deno Deploy
// Built-in test runner (no Jest/Vitest needed):
import { assertEquals, assertRejects } from "jsr:@std/assert@^1";

Deno.test("adds two numbers", () => {
  assertEquals(1 + 2, 3);
});

Deno.test("async test", async () => {
  const res = await fetch("http://localhost:8000/health");
  assertEquals(res.status, 200);
});

Deno.test({
  name: "skipped test",
  ignore: true,
  fn: () => {},
});

// Sub-tests (grouping):
Deno.test("UserService", async (t) => {
  await t.step("creates user", async () => {
    const user = await UserService.create({ name: "Alice" });
    assertEquals(user.name, "Alice");
  });
  await t.step("throws on duplicate email", async () => {
    await assertRejects(() => UserService.create({ email: "dupe@test.com" }));
  });
});

// deno test --watch --allow-net

// Benchmarking:
Deno.bench("JSON parse", () => {
  JSON.parse('{"name":"Alice","age":30}');
});

// Docker (single binary, tiny image):
FROM denoland/deno:2.1.0
WORKDIR /app
COPY deno.json deno.lock ./
RUN deno cache main.ts
COPY . .
EXPOSE 8000
CMD ["deno", "run", "--allow-net", "--allow-env", "--allow-read", "main.ts"]

# Compile to single binary:
deno compile --allow-net --allow-env --output=server main.ts

# Deno Deploy (zero-config edge deployment):
deployctl deploy --project=my-app main.ts

Track Deno releases at ReleaseRun. Related: Node.js Reference | TypeScript Reference | WebSocket Reference

🔍 Free tool: npm Package Health Checker — for npm-compatible packages in Deno, check any package for EOL status and known CVEs instantly.

Founded

2023 in London, UK

Contact

hello@releaserun.com