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/.

bash
# 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.

bash
deno --version

Output:

text
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.

bash
deno upgrade

Output:

text
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.

bash
deno <subcommand> [permissions] [args] file.ts -- [script-args]

Output: (none — exits 0 on success)

Essential commands

CommandPurpose
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/pkgAdd a JSR dep to deno.json imports.
deno remove <name>Remove a dependency.
deno fmtFormat files in place.
deno lintLint without fixing.
deno testRun the built-in test runner.
deno benchRun microbenchmarks.
deno check <file>Type-check without executing.
deno compile <file>Produce a single executable.
deno publishPublish to the JSR registry.
deno doc <file>Generate documentation from JSDoc.
deno replStart 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.

bash
# 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):

text
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.

bash
deno run script.ts

Output:

text
⚠️  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

FlagWhat 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-promptDisable 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).

typescript
// 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}!`);
bash
deno run server.ts

Output:

text
Welcome, Alice Dev!

Type-check without executing:

bash
deno check server.ts

Output:

text
Check file:///home/alice/server.ts

Override the default TypeScript config inline via deno.json:

jsonc
{
  "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.

jsonc
{
  "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).

bash
# List available tasks
deno task

# Run a task
deno task dev

# Pass extra args after --
deno task test -- --filter "User"

Output (deno task):

text
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.

bash
# 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):

text
Add @std/path - jsr:@std/path@^1.0.8

Once added, import by the bare specifier — no path or version:

typescript
// app.ts
import { join } from "@std/path";
import { assertEquals } from "@std/assert";
import _ from "lodash";

console.log(join("/home/alice", "docs", "file.txt"));
bash
deno run app.ts

Output:

text
/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.

typescript
// 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!"));
bash
deno run --allow-net server.ts

Output:

text
Listening on http://0.0.0.0:8000/

Force a re-fetch (bust the cache):

bash
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.

typescript
// 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);
bash
deno run --allow-read --allow-env server.ts

Output:

text
PID: 12345

npm: specifier semantics

SpecifierBehaviour
npm:lodashLatest version on npm.
npm:lodash@^4.17Semver range.
npm:@scope/pkg@4.0.0Pinned scoped package.
npm:react@latestTag (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.

bash
# 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):

text
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.

bash
# 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:

text
(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.

typescript
// 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));
});
bash
deno test --allow-read=./fixtures

Output:

text
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

FlagPurpose
--filter "<pattern>"Run tests by name match (substring or regex).
--coverage=cov/Emit V8 coverage to a directory; combine with deno coverage.
--parallelRun test files in parallel processes.
--watchRe-run on file change.
--fail-fast[=n]Stop after n failures (default 1).
--reporter=junitEmit JUnit XML for CI.
--docType-check JSDoc code examples too.

Coverage report:

bash
deno test --coverage=cov/
deno coverage cov/ --lcov --output=lcov.info

Output (deno coverage):

text
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.

typescript
// 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);
});
bash
deno bench

Output:

text
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.

bash
# 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:

text
Compile file:///home/alice/cli.ts to mycli

Compile target reference

--target valuePlatform
x86_64-unknown-linux-gnuLinux x64
aarch64-unknown-linux-gnuLinux ARM64
x86_64-apple-darwinmacOS Intel
aarch64-apple-darwinmacOS Apple Silicon
x86_64-pc-windows-msvcWindows 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.

bash
# Start REPL
deno repl

# One-shot inline script
deno eval 'console.log(Math.PI * 2)'

Output (deno eval):

text
6.283185307179586

REPL session:

bash
deno repl

Output:

text
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

  1. 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.
  2. --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.
  3. Permission prompts hang in CI — non-interactive shells must use explicit flags or --no-prompt. Add --no-prompt to your CI script and capture failures early.
  4. Mixing JSR and npm: versions of the same package — JSR @hono/hono and npm:hono resolve as distinct modules. Pick one or the type imports won't unify.
  5. 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.
  6. node_modules mode breaks performancenodeModulesDir: "auto" doubles install time and disk usage. Only enable it for packages that genuinely require a real node_modules (most do not in 2026).
  7. Deno.exit skips async cleanup — pending Promises and addEventListener("unload") handlers don't get to finish. Prefer letting main() return naturally.
  8. 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.

typescript
// 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}!`);
bash
deno compile --output hello cli.ts
./hello --name "Alice Dev"

Output:

text
Compile file:///home/alice/cli.ts to hello
Hello, Alice Dev!

Cross-compile to Linux ARM64 from a Mac:

bash
deno compile --target=aarch64-unknown-linux-gnu --output hello-linux-arm64 cli.ts
file hello-linux-arm64

Output:

text
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.

typescript
// 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 });
});
bash
deno run --allow-net=0.0.0.0:3000,api.github.com proxy.ts

Output:

text
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.

jsonc
// deno.json
{
  "tasks": {
    "ci": "deno fmt --check && deno lint && deno check **/*.ts && deno test --coverage=cov/"
  }
}
bash
deno task ci

Output:

text
$ 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.

typescript
// 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`);
bash
deno run --allow-net=api.github.com fetch-stars.ts denoland/deno

Output:

text
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.

jsonc
// deno.json
{
  "name": "@alicedev/greetings",
  "version": "0.1.0",
  "exports": "./mod.ts"
}
typescript
// mod.ts
export function greet(name: string): string {
  return `Hello, ${name}!`;
}
bash
deno publish --dry-run

Output:

text
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.