Skip to content
Blog

Deno: A Modern Runtime for Server-Side JavaScript

Deno 2 in production: the secure-by-default permission model, native TypeScript, the npm and JSR ecosystems, std modules, and the web-standard APIs that make server code boring to write.

Published on August 11, 2026

AI Assistant

Node.js gave us server-side JavaScript, and we’ve been paying its 2009 design taxes ever since: a module system bolted onto the language, a node_modules tree that swallows gigabytes, and code that can read any file or phone any host by default. The runtime question that was boring for a decade is interesting again in 2026, because Deno 2 turned the security model inside out and closed the ecosystem gap that once hurt adoption.

Deno is a drop-in runtime for Node developers built on V8 and Rust, with native TypeScript, web-standard APIs, a permission system that denies by default, and near-complete npm compatibility. Used in production at Slack, Netlify, and others, its pitch in 2026 is simple: the runtime that makes servers safe and simple.

In this post, you will learn Deno’s permission model and how to scope it, the built-in toolchain (format, lint, test, compile), web-standard server APIs including Deno.serve, how to mix npm: and jsr: packages, and what Deno KV and Deno Deploy add. Key technologies: Deno 2.x (2.9 as of mid-2026), the std library on JSR (@std/*), Deno KV, and Deno Deploy.

Prerequisites

  • A terminal. Deno is a single binary — no npm, no config file required to start.
  • Optional: Node.js knowledge to appreciate the comparison points.

Install and first run

Install on Windows with PowerShell, or scoop/winget; on macOS/Linux with the install script:

irm https://deno.land/install.ps1 | iex
deno --version   # deno 2.9.x

The first program is one file, no package.json, no build step:

// hello.ts
interface User { id: number; name: string; }
const user = await fetch("https://jsonplaceholder.typicode.com/users/1")
  .then((r) => r.json()) as User;
console.log(`Hello, ${user.name}!`);

Run it and watch what happens:

$ deno run hello.ts
error: Uncaught (in promise) PermissionDenied: Requires net access to
"jsonplaceholder.typicode.com", run again with --allow-net

$ deno run --allow-net=jsonplaceholder.typicode.com hello.ts
Hello, Leanne Graham!

That error is the product. Network, filesystem, environment — nothing is accessible by default; you grant exactly what the program needs.

The permission model

Permissions are Deno’s signature feature, and in 2026 they come in granular, per-resource flavors:

deno run --allow-net=api.example.com:443 --allow-read=./data server.ts
deno run --allow-env=DATABASE_URL --allow-sys=cpuLoad app.ts
  • --allow-read=<paths> — read only those paths (comma-separated).
  • --allow-net=<host[:port]> — network to specific hosts; the runner can prompt interactively during development.
  • --allow-env=<VARS> — specific environment variables.
  • --allow-sys, --allow-run, --allow-write, --allow-ffi — the rest, each scoped.
  • -A — everything (local development only).
  • --deny-read=/etc/passwd — explicit denials that override allows.

The modern way to lock it down is in deno.json, so permissions ship with the repo:

{
  "name": "@acme/web",
  "version": "1.0.0",
  "tasks": {
    "dev": "deno run --watch main.ts",
    "start": "deno run main.ts"
  },
  "permissions": {
    "net": ["api.example.com:443"],
    "read": ["./data"]
  }
}

For a supply-chain attack, default-deny is transformative: a compromised dependency literally cannot exfiltrate your files or environment unless you explicitly allowed it. That single property is why Deno’s model is a genuine security improvement over runtimes where full access is the default.

Native TypeScript and the built-in toolchain

TypeScript is not a plugin — Deno strips types and runs it directly, strict checks on, no tsc, no ts-node, no config. The toolchain is bundled too: deno fmt (formatter), deno lint, deno test (with type-checking), deno bench, deno compile (single-binary output), deno doc, and deno task for scripts.

Tests look like the language should:

import { assertEquals } from "jsr:@std/assert";
Deno.test("add", () => assertEquals(2 + 2, 4));
$ deno test
running 1 test from ./main_test.ts
add ... ok | 1 passed | 0 failed (2ms)

deno.json also hosts import maps, tasks, compiler options, and the deno.lock that makes installs reproducible.

Web-standard APIs: the server, without frameworks

Deno is a WinterCG and TC39 participant, so the server APIs are the browser APIs. Deno.serve is the entry point:

// server.ts
Deno.serve({ port: 8000 }, (req: Request) => {
  const url = new URL(req.url);
  if (url.pathname === "/api/users") {
    return Response.json({ users: ["Ada", "Grace", "Alan"] });
  }
  return new Response("Not Found", { status: 404 });
});

Running it with just --allow-net boots a server on http://localhost:8000/. The same code runs in a browser, Cloudflare Workers, and Deno Deploy. Fetch, Request/Response, EventSource, WebSocket, ReadableStream, crypto, and — stabilized in the 2.x line — Temporal for dates, all without imports. A server-sent-events feed is a ReadableStream and a Response:

Deno.serve((req) => new Response(new ReadableStream({
  start(c) {
    const id = setInterval(() => c.enqueue(`data: ${Date.now()}\n\n`), 1000);
    req.signal.addEventListener("abort", () => clearInterval(id));
  },
}), { headers: { "content-type": "text/event-stream" } }));

The ecosystem: npm and JSR together

The “my packages won’t run” objection is gone. Deno 2 supports millions of npm packages through the npm: specifier, reads a conventional package.json, and pairs them with JSR — the TypeScript-first registry whose packages ship types natively and publish to npm automatically:

import express from "npm:express@4";   // npm, unchanged
import { z } from "npm:zod";
import { serveFile } from "jsr:@std/http/file-server"; // JSR: types built in
import { parseArgs } from "jsr:@std/cli/parse-args";

Native C++ addons and packages that monkey-patch Node internals are the remaining exceptions — audit those before committing. Deno reports a modern Node-API version (NAPI 10) so version-gating packages see a current runtime.

Deno KV, Deno.cron, and Deno Deploy

Beyond the runtime, three platform pieces round out the story. Deno KV is a zero-config key-value store with transactions — no database to provision, identical locally and on Deno Deploy:

const kv = await Deno.openKv();
await kv.atomic()
  .check({ key: ["counters", "visits"], versionstamp: null })
  .set(["counters", "visits"], 0)
  .commit();

Deno.cron schedules jobs declaratively, and Deno Deploy hosts scripts at the edge with per-deploy permissions — the same deno.json permission list gates production, not just your laptop.

Putting It All Together

A runnable API server — typed route handlers, Temporal timestamps, a KV-backed counter, and a test suite — is in this gist: https://gist.github.com/redlinesoft/deno-api-server

$ deno run --allow-net --allow-read main.ts
Listening on http://localhost:8000/
$ curl http://localhost:8000/api/users
{"users":["Ada","Grace","Alan"]}
$ deno test
running 2 tests ... ok

Expected output: the server boots only after the permission flags match what the code touches; deno fmt normalizes the file; deno test reports type-checked passing tests; and the KV counter survives restarts because state lives in the platform, not a process.

Conclusion & Next Steps

You’ve seen why Deno is a serious 2026 runtime: default-deny permissions that contain supply-chain damage, zero-config native TypeScript, a complete built-in toolchain, web-standard server APIs, and both npm and JSR ecosystems. Next: run an existing Node CLI with deno run -R -E npm:some-tool, convert one service to Deno.serve, add Deno KV for state, and deploy it to Deno Deploy — the permissions file is the first thing a security reviewer will read.

References / Sources