Guide
Deno fundamentals explained
A third-party script in a CI pipeline reads your .env, phones home
over the network, and writes a file outside the project tree — and by default,
Node.js allows
all of that. Deno is Ryan Dahl’s second attempt at a JavaScript
server runtime: same V8 engine, but rewritten in Rust with
explicit permission flags, ES modules only,
and TypeScript built in. You run deno run --allow-net main.ts
instead of hoping npm install did not pull a compromised transitive
dependency. Deno is not a drop-in Node replacement for every legacy codebase,
but it is increasingly viable for new APIs, edge workers, CLIs, and internal
tools where security defaults matter. This guide covers the runtime architecture,
permission model, module resolution (URL imports, JSR, and npm:
specifiers), deno.json configuration, HTTP with Deno.serve,
testing and formatting, a Harbor Fleet webhook relay worked example, a runtime
decision table, common pitfalls, and a production checklist — alongside our
TypeScript fundamentals guide
and
REST API design guide.
What Deno is (and how it differs from Node)
Deno is a standalone JavaScript and TypeScript runtime —
not a framework. Like Node, it embeds V8 and uses an event-driven async model
backed by Tokio (Rust’s async runtime) instead of libuv. Unlike Node, Deno
ships a formatter (deno fmt), linter (deno lint),
test runner (deno test), and bundler without separate npm installs.
There is no node_modules folder by default: dependencies resolve
from URLs, the JSR registry, or npm packages via Deno’s native npm
compatibility layer.
Design goals that shape every API
- Secure by default — file, network, environment, and subprocess access require explicit flags or runtime grants.
- TypeScript first —
.tsfiles run without a compile step; type-checking is opt-in viadeno check. - Web-standard APIs —
fetch,Request/Response,WebSocket, andcrypto.subtlematch browser semantics, reducing context-switching. - Explicit configuration —
deno.json(ordeno.jsonc) centralizes tasks, imports, lint rules, and compiler options.
Deno 2.x (2024 onward) narrowed the gap with Node: improved npm and
node: built-in compatibility, deno install for global
CLIs, and workspace support for monorepos. Teams still on heavy native-addon
stacks (old bcrypt builds, custom C++ bindings) may hit friction;
greenfield services that speak HTTP and JSON are the sweet spot.
The permission model
Deno’s security model is its clearest differentiator. Without flags, a script can compute and print to stdout — nothing else. Common grants:
--allow-read[=path]— read files or directories (default deny).--allow-write[=path]— write or create files.--allow-net[=host:port]— outbound TCP/UDP; restrict to specific hosts in production manifests.--allow-env[=VAR]— read environment variables (API keys, database URLs).--allow-run[=cmd]— spawn subprocesses (use sparingly).--allow-ffi— load foreign function interfaces (rare; high risk).
In development, --allow-all (or -A) disables the sandbox
— convenient, but defeats the purpose. Production deploys should list minimal
scopes: a webhook receiver needs --allow-net on its listen port and
perhaps one upstream host, not blanket internet access. Deno Deploy and container
entrypoints encode these grants in the platform config so operators audit permissions
like firewall rules.
Compare with Node, where any required module inherits full process privileges.
Deno does not eliminate supply-chain risk, but it limits blast radius when a
dependency goes rogue: a library without --allow-net cannot exfiltrate
data even if imported.
Modules, JSR, and npm compatibility
Deno uses ECMAScript modules only — import and
export, no require(). Classic patterns:
// URL import (version pinned)
import { Application } from "https://deno.land/x/oak@v17.0.0/mod.ts";
// JSR — Deno's modern registry (scoped packages)
import { z } from "jsr:@std/zod@3.23.8";
// npm package — resolved and cached by Deno, no manual node_modules
import express from "npm:express@4.21.0";
JSR (JavaScript Registry) publishes TypeScript-first packages with
documentation and semver. Import maps in deno.json
alias long URLs to short names so refactors do not chase strings across files:
{
"imports": {
"@/": "./src/",
"zod": "jsr:@std/zod@^3.23.8"
},
"tasks": {
"dev": "deno run --allow-net --watch main.ts",
"test": "deno test --allow-read"
}
}
Deno caches remote modules in a global cache directory (overridable via
DENO_DIR). Lockfiles (deno.lock) pin dependency graphs
for reproducible CI — analogous to package-lock.json but
capturing URL, JSR, and npm resolutions in one file. For teams migrating from
Node, deno install can hydrate a local node_modules
when a library still expects Node resolution semantics.
HTTP servers, std library, and tooling
Modern Deno services use Deno.serve() — a built-in HTTP/HTTPS
server on web-standard Request/Response objects:
Deno.serve({ port: 8080 }, async (req) => {
const url = new URL(req.url);
if (url.pathname === "/health") {
return new Response(JSON.stringify({ ok: true }), {
headers: { "content-type": "application/json" },
});
}
return new Response("Not found", { status: 404 });
});
Frameworks like Oak (middleware stack), Hono
(edge-friendly, also runs on Cloudflare Workers), and Fresh
(file-based SSR with islands architecture) layer routing and templating on top.
The standard library (@std/ on JSR) provides tested helpers for
streams, encoding, path manipulation, and testing assertions — prefer it
over copying utility snippets from blog posts.
deno test runs tests with optional permissions per file, supports
snapshots, and parallelizes across cores. deno fmt and
deno lint enforce consistent style without Prettier/ESLint
boilerplate — though ESLint can still run via deno lint
plugin compatibility when you need custom rules.
Worked example: Harbor Fleet webhook relay
Harbor Fleet operates a small dispatch service that accepts signed webhooks from
a payment provider and forwards normalized events to an internal queue. The team
chose Deno for three reasons: TypeScript without a build step, tight
--allow-net scoping, and a 12 MB container image.
Architecture
- Ingress —
Deno.serveon port 8787 behind nginx TLS termination. - Verification — HMAC-SHA256 signature check using
crypto.subtle(no extra deps). - Transform — map provider payload to an internal
TripPaidevent schema with Zod validation. - Egress —
fetchPOST to the queue API (single allowed host in permissions).
Their deno.json task for production:
"tasks": {
"start": "deno run --allow-net=0.0.0.0:8787,queue.harbor.internal --allow-env=WEBHOOK_SECRET,QUEUE_TOKEN main.ts"
}
On deploy, the CI pipeline runs deno test --allow-read (unit tests
with fixture payloads), deno check (full type graph), then builds
a distroless image copying only main.ts, deno.json,
and deno.lock. Cold start on Deno Deploy edge is under 50 ms because
there is no framework bootstrapping a massive dependency tree. When latency
requirements tightened, they added idempotency keys and structured JSON logging
— patterns covered in our
idempotency guide
and
structured logging guide.
Runtime decision table
| Runtime | Best for | Trade-offs |
|---|---|---|
| Deno | New APIs, edge functions, secure CLIs, TypeScript services without bundlers | Smaller hiring pool than Node; some npm packages need compatibility shims |
| Node.js LTS | Largest ecosystem, native addons, existing Express/Nest codebases | No permission sandbox; TypeScript needs ts-node or compile step |
| Bun | Speed-sensitive tooling, fast test runs, drop-in Node migration experiments | Younger production track record; API surface still shifting |
| Serverless managed (Deno Deploy, Workers) | Global low-latency HTTP, minimal ops, per-request billing | Vendor limits on CPU time, no long-lived background workers |
Common pitfalls
- --allow-all in production — negates Deno’s security story; scope each flag to hosts and paths.
- Unpinned URL imports —
@latestURLs break CI silently; use lockfiles and semver pins. - Assuming full Node API parity —
node:fscompatibility improves but edge cases (custom native modules) still fail. - Blocking the event loop — same as Node: synchronous CPU work stalls all requests; offload heavy compute.
- Ignoring
deno checkin CI — runtime does not type-check by default; uncaught type errors ship to prod. - Mixing permission models in monorepos — document which workspace packages need net vs read flags.
- Secrets in
--allow-envwithout vault — permissions limit which vars are read, not exfiltration if net is wide open.
Practitioner checklist
- Pin Deno version in CI (e.g.
denoland/deno:2.1.4Docker tag) and locally via.tool-versionsor mise. - Commit
deno.lock; rundeno cache --lock=deno.lockin deploy pipelines. - List minimal permission flags in
deno.jsontasks — not scattered in README examples. - Run
deno fmt,deno lint,deno test, anddeno checkon every pull request. - Prefer JSR and pinned npm specifiers over unpinned
deno.land/xURLs. - Use web-standard
fetchandResponsefor portability across Deno Deploy and containers. - Place services behind a reverse proxy for TLS and rate limiting.
- Log structured JSON; handle
SIGTERMby draining in-flight requests before exit. - Validate webhook signatures before parsing JSON bodies; reject oversized payloads at the edge.
- Re-evaluate Node compatibility quarterly — Deno’s npm layer improves fast.
Key takeaways
- Deno is a secure-by-default JavaScript/TypeScript runtime built on V8 and Rust with explicit permission flags.
- ESM-only imports, JSR, and
npm:specifiers replace defaultnode_modulesworkflows. deno.jsoncentralizes tasks, import maps, and tooling; lockfiles keep CI reproducible.Deno.serveand web-standard APIs make HTTP services portable to edge and containers.- Choose Deno for greenfield services where security defaults and TypeScript ergonomics outweigh legacy npm addon needs.
Related reading
- Node.js fundamentals explained — the runtime Deno evolved from: libuv, npm, and the event loop model
- TypeScript fundamentals explained — types, generics, and strict mode that Deno runs natively
- REST API design explained — routes, status codes, and versioning for Deno HTTP services
- Docker fundamentals explained — packaging Deno apps in minimal container images