Skip to main content
Technology & EngineeringDeno Bun183 lines

Deno Basics

Deno runtime fundamentals including permissions, module system, and built-in tooling

Quick Summary17 lines
You are an expert in Deno runtime fundamentals for building secure, modern applications with first-class TypeScript support.

## Key Points

- **Pin dependency versions** in import URLs or use an import map in `deno.json` so builds are reproducible.
- **Use JSR** (`jsr:`) as the primary registry; it offers type-checked publishing and is Deno-native.
- **Scope permissions narrowly** — grant `--allow-read=./data` instead of blanket `--allow-read`.
- **Use `deno.lock`** (generated automatically) and commit it to version control for deterministic installs.
- **Prefer Web Standard APIs** (`fetch`, `Request`, `Response`, `crypto`, `ReadableStream`) — they are built in and portable.
- **Run `deno fmt` and `deno lint`** in CI; they require zero configuration.
- **Forgetting permissions flags** — your script will throw a `PermissionDenied` error at runtime, not at import time. Test with the same flags you'll use in production.
- **Using bare specifiers without an import map** — `import "lodash"` will fail unless you map it in `deno.json` imports or prefix with `npm:`.
- **Caching surprises** — Deno caches remote modules globally. Run `deno cache --reload mod.ts` to force re-download after upstream changes.
- **Mixing `Deno.*` APIs with Node APIs** — prefer one or the other. Using both leads to confusing permission and compatibility issues.
- **Assuming `__dirname` and `__filename` exist** — these are Node-isms. Use `import.meta.dirname` and `import.meta.filename` in Deno 1.40+ or `import.meta.url` with `URL` for older versions.
skilldb get deno-bun-skills/Deno BasicsFull skill: 183 lines
Paste into your CLAUDE.md or agent config

Deno Basics — Modern JS Runtimes

You are an expert in Deno runtime fundamentals for building secure, modern applications with first-class TypeScript support.

Overview

Deno is a secure runtime for JavaScript and TypeScript created by Ryan Dahl. It runs TypeScript natively without a compilation step, uses URL-based ES module imports, and enforces an explicit permissions model so that scripts cannot access the filesystem, network, or environment unless granted.

Core Concepts

Permissions Model

Deno is secure by default. All access to the filesystem, network, environment variables, and subprocesses must be explicitly granted via CLI flags.

FlagGrants
--allow-read[=path]Filesystem read access
--allow-write[=path]Filesystem write access
--allow-net[=host]Network access
--allow-env[=VAR]Environment variable access
--allow-run[=cmd]Subprocess execution
--allow-ffiForeign function interface
-AAll permissions (development only)

Module System

Deno uses URL-based ES module imports. There is no node_modules directory or package.json required, though both are supported via compatibility features.

// Import from the Deno standard library
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";

// Import from JSR (recommended registry)
import { z } from "jsr:@zod/zod@3";

// Import from npm (compatibility layer)
import chalk from "npm:chalk@5";

deno.json Configuration

{
  "tasks": {
    "dev": "deno run --watch --allow-net main.ts",
    "test": "deno test --allow-read"
  },
  "imports": {
    "@std/http": "jsr:@std/http@1",
    "@std/assert": "jsr:@std/assert@1"
  },
  "compilerOptions": {
    "strict": true
  }
}

Built-in Tooling

Deno ships with a formatter, linter, test runner, bundler, and documentation generator:

deno fmt           # Format source files
deno lint          # Lint source files
deno test          # Run tests
deno doc mod.ts    # Generate documentation
deno bench         # Run benchmarks
deno compile       # Compile to standalone binary

Implementation Patterns

HTTP Server

Deno.serve({ port: 8000 }, (req: Request): Response => {
  const url = new URL(req.url);

  if (url.pathname === "/api/health") {
    return Response.json({ status: "ok" });
  }

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

File Operations

// Reading a file
const content = await Deno.readTextFile("./data.json");
const data = JSON.parse(content);

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

// Streaming a large file
const file = await Deno.open("./large.csv", { read: true });
const readable = file.readable;
for await (const chunk of readable) {
  // process chunk
}

Testing

import { assertEquals, assertThrows } from "jsr:@std/assert";

Deno.test("addition works", () => {
  assertEquals(2 + 2, 4);
});

Deno.test("async operation", async () => {
  const resp = await fetch("https://api.example.com/data");
  assertEquals(resp.status, 200);
});

Deno.test({
  name: "requires file access",
  permissions: { read: true },
  fn: async () => {
    const text = await Deno.readTextFile("./fixture.txt");
    assertEquals(text.length > 0, true);
  },
});

Environment Variables

// Requires --allow-env
const port = Deno.env.get("PORT") ?? "8000";
const dbUrl = Deno.env.get("DATABASE_URL");

if (!dbUrl) {
  console.error("DATABASE_URL is required");
  Deno.exit(1);
}

Core Philosophy

Deno was created to address the design mistakes of Node.js, as identified by Node's own creator. The three pillars of Deno's philosophy are security by default, web standards alignment, and first-class TypeScript support. Each of these is a reaction to a specific pain point: Node's unchecked access to the filesystem and network, Node's divergence from browser APIs, and Node's reliance on external tooling for TypeScript.

The permissions model is Deno's most distinctive feature and its most important design decision. By requiring explicit flags for filesystem, network, and environment access, Deno makes the security surface of every script visible at the command line. You can see exactly what a script can do before running it, and you can grant the minimum permissions needed. This is a stark contrast to Node, where any npm package can read your filesystem, make network requests, and access environment variables without restriction.

Deno's module system uses URLs as identifiers, which eliminates the node_modules directory, the package.json configuration file, and the centralized npm registry as mandatory dependencies. Modules are cached globally, referenced by URL, and resolved without a resolution algorithm. This is simpler and more predictable, but it also shifts the burden of version management to the developer, which is why import maps in deno.json and the JSR registry exist to provide familiar ergonomics without the complexity of node_modules.

Anti-Patterns

  • Running everything with -A (all permissions). Granting blanket permissions in development creates a false sense of what the script actually needs. When you deploy with scoped permissions, missing grants cause runtime crashes. Develop with the same permission flags you will use in production.

  • Using bare specifiers without an import map. Writing import "lodash" fails in Deno unless the import map in deno.json maps it to an npm: or JSR specifier. Always define mappings for bare specifiers to avoid confusing resolution errors.

  • Mixing Deno-native and Node APIs for the same concern. Using Deno.readTextFile in one file and import { readFile } from "node:fs/promises" in another for the same purpose creates inconsistency. Choose one approach per concern and stick with it throughout the project.

  • Assuming __dirname and __filename exist. These are Node-isms that do not exist in standard ESM. Use import.meta.dirname and import.meta.filename (Deno 1.40+) or construct paths from import.meta.url.

  • Forgetting that Deno caches remote modules globally. Once a URL-imported module is cached, Deno uses the cached version indefinitely. After upstream changes, you must run deno cache --reload to force a re-download. Stale caches can cause hours of debugging.

Best Practices

  • Pin dependency versions in import URLs or use an import map in deno.json so builds are reproducible.
  • Use JSR (jsr:) as the primary registry; it offers type-checked publishing and is Deno-native.
  • Scope permissions narrowly — grant --allow-read=./data instead of blanket --allow-read.
  • Use deno.lock (generated automatically) and commit it to version control for deterministic installs.
  • Prefer Web Standard APIs (fetch, Request, Response, crypto, ReadableStream) — they are built in and portable.
  • Run deno fmt and deno lint in CI; they require zero configuration.

Common Pitfalls

  • Forgetting permissions flags — your script will throw a PermissionDenied error at runtime, not at import time. Test with the same flags you'll use in production.
  • Using bare specifiers without an import mapimport "lodash" will fail unless you map it in deno.json imports or prefix with npm:.
  • Caching surprises — Deno caches remote modules globally. Run deno cache --reload mod.ts to force re-download after upstream changes.
  • Mixing Deno.* APIs with Node APIs — prefer one or the other. Using both leads to confusing permission and compatibility issues.
  • Assuming __dirname and __filename exist — these are Node-isms. Use import.meta.dirname and import.meta.filename in Deno 1.40+ or import.meta.url with URL for older versions.

Install this skill directly: skilldb add deno-bun-skills

Get CLI access →