cheat sheet
Deno
Deno is a secure, modern JavaScript and TypeScript runtime built on V8 with permission-based sandboxing, built-in TypeScript, a curated standard library on JSR, and a complete toolchain (fmt, lint, test, bench, compile).
Deno — Secure JavaScript & TypeScript Runtime
What it is
Deno is a JavaScript and TypeScript runtime built on V8 and Rust by Ryan Dahl — the same person who created Node.js — that re-thinks the parts of Node that aged poorly. It runs TypeScript directly (no tsc), sandboxes every script behind explicit permission flags (--allow-net, --allow-read), ships a complete toolchain in one binary (deno fmt, deno lint, deno test, deno bench, deno compile), and pulls dependencies from URLs or the JSR registry instead of node_modules. Reach for Deno when you want a batteries-included, secure-by-default runtime; the main alternatives are Node.js (the incumbent, larger ecosystem) and Bun (faster, Node-compatible, no sandboxing).
Install
Deno ships pre-built binaries for Linux, macOS, and Windows. The installer drops deno into ~/.deno/bin/.
# Linux / macOS / WSL — official installer
curl -fsSL https://deno.land/install.sh | sh
# macOS — Homebrew
brew install deno
# Windows — PowerShell
powershell -c "irm https://deno.land/install.ps1 | iex"
# Cargo (from Rust source)
cargo install deno --locked
Output: (none — exits 0 on success)
Verify the install — deno --version prints Deno, V8, and TypeScript versions.
deno --version
Output:
deno 2.1.4 (stable, release, x86_64-unknown-linux-gnu)
v8 13.0.245.12
typescript 5.6.2
Self-update — Deno manages its own binary; --canary opts in to nightly builds.
deno upgrade
Output:
Looking up latest version
Found latest version 2.1.6
Checking https://github.com/denoland/deno/releases/...
Deno is upgrading to version 2.1.6
Upgraded successfully to Deno 2.1.6
Syntax
Deno is a single binary with subcommands. The most common pattern is deno run --allow-* file.ts, but deno task (run package scripts), deno test, and deno fmt are equally first-class.
deno <subcommand> [permissions] [args] file.ts -- [script-args]
Output: (none — exits 0 on success)
Essential commands
| Command | Purpose |
|---|---|
deno run <file> | Execute a JS/TS file with permission flags. |
deno task <name> | Run a task from deno.json. |
deno install <pkg> | Install an npm or JSR dependency. |
deno add jsr:@scope/pkg | Add a JSR dep to deno.json imports. |
deno remove <name> | Remove a dependency. |
deno fmt | Format files in place. |
deno lint | Lint without fixing. |
deno test | Run the built-in test runner. |
deno bench | Run microbenchmarks. |
deno check <file> | Type-check without executing. |
deno compile <file> | Produce a single executable. |
deno publish | Publish to the JSR registry. |
deno doc <file> | Generate documentation from JSDoc. |
deno repl | Start an interactive REPL. |
Permissions model
Every Deno script runs in a sandbox with zero capabilities by default — no file system, no network, no environment variables, no subprocesses. You opt in per-capability with --allow-* flags, optionally restricting scope (e.g. --allow-net=example.com). This is the single biggest behavioural difference from Node, where a script can do anything the OS user can.
# Zero permissions — fetch() throws "Requires net access"
deno run script.ts
# Grant network only
deno run --allow-net script.ts
# Grant network to a specific host
deno run --allow-net=api.example.com script.ts
# Read access scoped to a directory
deno run --allow-read=./data script.ts
# Read + write
deno run --allow-read --allow-write script.ts
# Environment variables (allowlist named vars)
deno run --allow-env=NODE_ENV,DATABASE_URL script.ts
# Subprocess spawning
deno run --allow-run=git,ls script.ts
# Foreign function interface (FFI)
deno run --allow-ffi=./libsqlite.so script.ts
# All permissions (escape hatch — equivalent to running as Node)
deno run -A script.ts
deno run --allow-all script.ts
Output (running without --allow-net):
error: Uncaught (in promise) NotCapable: Requires net access to "api.example.com",
run again with the --allow-net flag
Permission prompts
If you omit -A but interactive stdin is attached, Deno prompts before granting access. Useful for one-off scripts where you want a security review but no manual flag bookkeeping.
deno run script.ts
Output:
⚠️ Deno requests net access to "api.example.com".
├ Requested by `fetch()` API.
├ Run again with --allow-net to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net access) ›
Granular flag reference
| Flag | What it gates |
|---|---|
--allow-read[=paths] | Deno.readFile, Deno.readTextFile, fs.readFileSync, etc. |
--allow-write[=paths] | Deno.writeFile, file deletes, mkdir. |
--allow-net[=hosts] | fetch, sockets, Deno.connect, Deno.listen. |
--allow-env[=names] | Deno.env.get, process.env. |
--allow-run[=names] | Deno.Command (subprocess). |
--allow-sys[=apis] | OS info: Deno.hostname, Deno.osRelease. |
--allow-ffi[=libs] | Native library loading via Deno.dlopen. |
--allow-import[=urls] | Remote import statements (HTTPS URLs). |
--no-prompt | Disable interactive prompts — for CI, fails closed. |
Built-in TypeScript
Deno transpiles .ts, .tsx, and .mts directly — no tsconfig.json, no separate compiler. Types are checked the first time a file is run and cached; pass --check to force a fresh check or --no-check to skip type checking entirely (faster startup, runtime errors only).
// server.ts — TypeScript with no setup
interface User {
name: string;
email: string;
}
const user: User = { name: "Alice Dev", email: "alice@example.com" };
console.log(`Welcome, ${user.name}!`);
deno run server.ts
Output:
Welcome, Alice Dev!
Type-check without executing:
deno check server.ts
Output:
Check file:///home/alice/server.ts
Override the default TypeScript config inline via deno.json:
{
"compilerOptions": {
"lib": ["deno.window", "dom"],
"strict": true,
"noUncheckedIndexedAccess": true
}
}
Output: (none — exits 0 on success)
deno.json (project config)
deno.json (or deno.jsonc) is Deno's equivalent of package.json — declares tasks, dependency imports, lint/fmt config, and TS compiler options in one place. Detected automatically; no --config needed unless you keep it elsewhere.
{
"name": "@alicedev/myapp",
"version": "0.1.0",
"exports": "./mod.ts",
"tasks": {
"dev": "deno run --watch --allow-net --allow-env server.ts",
"test": "deno test --allow-read",
"build": "deno compile --allow-net --output dist/server server.ts"
},
"imports": {
"@std/assert": "jsr:@std/assert@^1.0.0",
"@std/path": "jsr:@std/path@^1.0.0",
"hono": "jsr:@hono/hono@^4.6.0",
"lodash": "npm:lodash@^4.17.21"
},
"fmt": { "lineWidth": 100, "indentWidth": 2 },
"lint": { "rules": { "tags": ["recommended"] } },
"exclude": ["dist/", "coverage/"]
}
Output: (none — exits 0 on success)
deno task — script runner
deno task is the equivalent of npm run. Lists tasks with no argument, runs the named task otherwise. Tasks can chain with && and || cross-platform (Deno has its own shell parser, no bash needed on Windows).
# List available tasks
deno task
# Run a task
deno task dev
# Pass extra args after --
deno task test -- --filter "User"
Output (deno task):
Available tasks:
- dev
deno run --watch --allow-net --allow-env server.ts
- test
deno test --allow-read
- build
deno compile --allow-net --output dist/server server.ts
JSR — the modern registry
JSR (jsr.io) is a TypeScript-first package registry built by the Deno team and usable from Node, Bun, and Deno. Packages are versioned, scoped (@scope/name), and shipped with both source TypeScript and pre-generated .d.ts for fast type-checking. Prefer JSR over npm: specifiers when a JSR equivalent exists.
# Add a JSR package — writes to deno.json imports
deno add jsr:@std/path
deno add jsr:@std/assert@^1.0.0
# Add an npm package
deno add npm:lodash
# Remove
deno remove @std/path
Output (deno add jsr:@std/path):
Add @std/path - jsr:@std/path@^1.0.8
Once added, import by the bare specifier — no path or version:
// app.ts
import { join } from "@std/path";
import { assertEquals } from "@std/assert";
import _ from "lodash";
console.log(join("/home/alice", "docs", "file.txt"));
deno run app.ts
Output:
/home/alice/docs/file.txt
Direct URL imports (no install step)
Deno also supports importing from any HTTPS URL — the original Deno ergonomic. Caches on first load, re-fetches with --reload. Useful for one-off scripts where you don't want a deno.json.
// no install — just import a versioned URL
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";
serve((_req) => new Response("Hello Alice Dev!"));
deno run --allow-net server.ts
Output:
Listening on http://0.0.0.0:8000/
Force a re-fetch (bust the cache):
deno run --reload --allow-net server.ts
Output: (none — exits 0 on success)
Node compatibility
Deno 2 reads package.json, node_modules, and npm packages via the npm: specifier — most Node libraries run unchanged. Built-ins use the same node: prefix as Node.
// Node-style imports both work
import express from "npm:express@^4.19.0";
import { readFile } from "node:fs/promises";
import process from "node:process";
const text = await readFile("./config.json", "utf8");
console.log("PID:", process.pid);
deno run --allow-read --allow-env server.ts
Output:
PID: 12345
npm: specifier semantics
| Specifier | Behaviour |
|---|---|
npm:lodash | Latest version on npm. |
npm:lodash@^4.17 | Semver range. |
npm:@scope/pkg@4.0.0 | Pinned scoped package. |
npm:react@latest | Tag (latest / next / beta). |
Deno installs npm packages into a global cache (~/.cache/deno/npm) — no node_modules until you opt in with nodeModulesDir: "auto" in deno.json (required by a handful of native-binding packages).
deno fmt — formatter
deno fmt formats JS, TS, JSX, TSX, JSON, JSONC, and Markdown files in place. Zero config required; tweak fmt.lineWidth, fmt.indentWidth, fmt.singleQuote, and fmt.useTabs in deno.json if defaults don't fit.
# Format every supported file under the cwd
deno fmt
# Format a single file
deno fmt src/server.ts
# Check formatting without writing (CI-friendly — exits non-zero if changes needed)
deno fmt --check
Output (deno fmt --check with issues):
from src/server.ts:
3 | const user = { name: "Alice Dev", email: "alice@example.com" }
+ const user = { name: "Alice Dev", email: "alice@example.com" };
Found 1 not formatted file in 12 files
deno lint — linter
deno lint runs a Rust-implemented linter (no ESLint config or plugin install). Rules tagged recommended are on by default; opt-in or disable additional rules in deno.json under lint.rules.
# Lint everything
deno lint
# Lint one file
deno lint src/server.ts
# List all available rules
deno lint --rules
# JSON output for editor integrations
deno lint --json
Output:
(no-unused-vars) `oldVar` is never used
at file:///home/alice/src/server.ts:5:7
hint: If this is intentional, prefix it with an underscore like `_oldVar`
help: for further information visit https://lint.deno.land/rules/no-unused-vars
Found 1 problem (0 fixable via --fix)
deno test — test runner
deno test runs *_test.ts, *.test.ts, and test*.ts files using a Jest-shaped API (no separate framework install). Tests can have steps, set permissions per-test, and produce JUnit / TAP output for CI.
// math_test.ts
import { assertEquals } from "@std/assert";
Deno.test("add() adds positives", () => {
assertEquals(2 + 3, 5);
});
Deno.test("permission scoped", {
permissions: { read: ["./fixtures"] },
}, async () => {
const data = await Deno.readTextFile("./fixtures/sample.txt");
assertEquals(data.trim(), "hello");
});
Deno.test("nested steps", async (t) => {
await t.step("step 1", () => assertEquals(1, 1));
await t.step("step 2", () => assertEquals(2, 2));
});
deno test --allow-read=./fixtures
Output:
running 3 tests from ./math_test.ts
add() adds positives ... ok (3ms)
permission scoped ... ok (7ms)
nested steps ...
step 1 ... ok (0ms)
step 2 ... ok (0ms)
nested steps ... ok (1ms)
ok | 3 passed (2 steps) | 0 failed (12ms)
Useful test flags
| Flag | Purpose |
|---|---|
--filter "<pattern>" | Run tests by name match (substring or regex). |
--coverage=cov/ | Emit V8 coverage to a directory; combine with deno coverage. |
--parallel | Run test files in parallel processes. |
--watch | Re-run on file change. |
--fail-fast[=n] | Stop after n failures (default 1). |
--reporter=junit | Emit JUnit XML for CI. |
--doc | Type-check JSDoc code examples too. |
Coverage report:
deno test --coverage=cov/
deno coverage cov/ --lcov --output=lcov.info
Output (deno coverage):
file:///home/alice/math.ts 100.000% (8/8 lines)
file:///home/alice/user.ts 91.667% (11/12 lines)
deno bench — microbenchmarks
deno bench runs *_bench.ts / *.bench.ts files using the same harness shape as deno test but for performance comparisons. Each Deno.bench(...) call gets statistical sampling with iterations chosen automatically.
// hash_bench.ts
import { crypto } from "@std/crypto";
const input = new TextEncoder().encode("a".repeat(1024));
Deno.bench("sha-256", { group: "hash", baseline: true }, async () => {
await crypto.subtle.digest("SHA-256", input);
});
Deno.bench("sha-512", { group: "hash" }, async () => {
await crypto.subtle.digest("SHA-512", input);
});
deno bench
Output:
benchmark time (avg) iter/s (min … max)
-----------------------------------------------------
sha-256 4.21 µs/iter 237,529 (4.05 µs … 7.81 µs)
sha-512 6.14 µs/iter 162,866 (5.98 µs … 9.42 µs)
summary
sha-256
1.46x faster than sha-512
deno compile — single-binary executables
deno compile bundles the runtime, the entry script, and every imported module into one cross-platform executable — no Deno install required on the target machine. Permissions are baked in at compile time.
# Compile for the current platform
deno compile --allow-net --output mycli cli.ts
# Cross-compile from macOS to Linux ARM64
deno compile --allow-net --target=aarch64-unknown-linux-gnu --output mycli-linux cli.ts
# Cross-compile to Windows
deno compile --allow-net --target=x86_64-pc-windows-msvc --output mycli.exe cli.ts
# Include an asset (read at runtime via Deno.readTextFile from $DENO_DIR)
deno compile --include=./templates --allow-read=./templates --output mycli cli.ts
Output:
Compile file:///home/alice/cli.ts to mycli
Compile target reference
--target value | Platform |
|---|---|
x86_64-unknown-linux-gnu | Linux x64 |
aarch64-unknown-linux-gnu | Linux ARM64 |
x86_64-apple-darwin | macOS Intel |
aarch64-apple-darwin | macOS Apple Silicon |
x86_64-pc-windows-msvc | Windows x64 |
REPL and deno eval
deno repl opens an interactive shell with autocomplete and history. deno eval runs a one-liner — useful for shell pipelines.
# Start REPL
deno repl
# One-shot inline script
deno eval 'console.log(Math.PI * 2)'
Output (deno eval):
6.283185307179586
REPL session:
deno repl
Output:
Deno 2.1.4
exit using ctrl+d, ctrl+c, or close()
REPL is running with all permissions allowed.
To specify permissions, run `deno repl` with allow flags.
> const x = [1, 2, 3]
undefined
> x.reduce((a, b) => a + b, 0)
6
> Deno.version
{ deno: "2.1.4", v8: "13.0.245.12", typescript: "5.6.2" }
Common pitfalls
- Forgetting permission flags — every script needs explicit
--allow-*for I/O. The error message tells you which flag is missing; copy-paste it back into the command. --allow-all(-A) defeats the sandbox — convenient in dev, dangerous in prod. Use scoped flags (--allow-net=api.example.com) for anything that runs unattended.- Permission prompts hang in CI — non-interactive shells must use explicit flags or
--no-prompt. Add--no-promptto your CI script and capture failures early. - Mixing JSR and
npm:versions of the same package — JSR@hono/honoandnpm:honoresolve as distinct modules. Pick one or the type imports won't unify. - Cached remote imports stick around — Deno caches by URL. After upstream changes,
deno run --reload(or--reload=https://deno.land/std) to bust the cache. node_modulesmode breaks performance —nodeModulesDir: "auto"doubles install time and disk usage. Only enable it for packages that genuinely require a realnode_modules(most do not in 2026).Deno.exitskips async cleanup — pending Promises andaddEventListener("unload")handlers don't get to finish. Prefer lettingmain()return naturally.- Compiled binaries are bigger than you expect — ~80 MB minimum because the V8 + TypeScript + std runtime is embedded. That's the cost of zero-dependency distribution; for small CLIs consider Bun's
bun build --compile(~60 MB).
Real-world recipes
Small CLI distributed as a standalone binary
The pitch: a TypeScript CLI you can deno compile once and hand to anyone — no runtime install, no npm install, just ./mycli.
// cli.ts
import { parseArgs } from "jsr:@std/cli/parse-args";
const args = parseArgs(Deno.args, {
string: ["name"],
default: { name: "world" },
});
console.log(`Hello, ${args.name}!`);
deno compile --output hello cli.ts
./hello --name "Alice Dev"
Output:
Compile file:///home/alice/cli.ts to hello
Hello, Alice Dev!
Cross-compile to Linux ARM64 from a Mac:
deno compile --target=aarch64-unknown-linux-gnu --output hello-linux-arm64 cli.ts
file hello-linux-arm64
Output:
hello-linux-arm64: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked
HTTP server with strict per-host net permissions
Only allow outbound connections to a single API endpoint — defence-in-depth for credential-stealing supply chain attacks.
// proxy.ts
Deno.serve({ port: 3000 }, async (req) => {
const url = new URL(req.url);
if (url.pathname === "/star-count") {
const res = await fetch("https://api.github.com/repos/denoland/deno");
const data = await res.json();
return Response.json({ stars: data.stargazers_count });
}
return new Response("Not Found", { status: 404 });
});
deno run --allow-net=0.0.0.0:3000,api.github.com proxy.ts
Output:
Listening on http://0.0.0.0:3000/
If a third-party dep tried fetch("https://evil.example.com"), Deno would deny it — api.github.com is the only allowed outbound host.
Lint, format, test, type-check pipeline
Run the whole toolchain in one task. CI-friendly because fmt --check exits non-zero on unformatted code.
// deno.json
{
"tasks": {
"ci": "deno fmt --check && deno lint && deno check **/*.ts && deno test --coverage=cov/"
}
}
deno task ci
Output:
$ deno fmt --check && deno lint && deno check **/*.ts && deno test --coverage=cov/
Checked 23 files
Checked 23 files
Check file:///home/alice/myapp/server.ts
running 12 tests from ./...
ok | 12 passed | 0 failed (218ms)
Migrate a one-off Node script to Deno
Replace require / import with npm: or JSR specifiers, swap process.argv for Deno.args, and run with explicit permissions — no package.json needed.
// fetch-stars.ts (was a Node script)
const [repo = "denoland/deno"] = Deno.args;
const res = await fetch(`https://api.github.com/repos/${repo}`);
const { stargazers_count } = await res.json();
console.log(`${repo}: ${stargazers_count} stars`);
deno run --allow-net=api.github.com fetch-stars.ts denoland/deno
Output:
denoland/deno: 97624 stars
Publish a TypeScript library to JSR
JSR ships your TypeScript source directly — no tsc build step, no dist/ directory. Slug + version + exports in deno.json, then deno publish.
// deno.json
{
"name": "@alicedev/greetings",
"version": "0.1.0",
"exports": "./mod.ts"
}
// mod.ts
export function greet(name: string): string {
return `Hello, ${name}!`;
}
deno publish --dry-run
Output:
Checking for slow types in the public API...
Check file:///home/alice/greetings/mod.ts
Publishing @alicedev/greetings@0.1.0 ...
Would have published @alicedev/greetings@0.1.0 (dry run)
Once verified, drop --dry-run to ship for real — Deno opens a browser to OAuth into JSR.