Tools

Deno: The Secure JavaScript Runtime

Mayur Dabhi
Mayur Dabhi
June 12, 2026
14 min read

In 2018, Ryan Dahl — the original creator of Node.js — stood on stage at JSConf EU and gave a talk titled "10 Things I Regret About Node.js." He acknowledged the design mistakes baked into Node from its early days: no security model, a messy module system, callback hell, and a package manager (npm) that had grown into an unwieldy monolith. Two years later, he released Deno — a new JavaScript and TypeScript runtime built from the ground up to fix those regrets. This guide explores everything you need to know about Deno: what makes it different, how to use it, and when it makes sense to reach for it over Node.js.

Why Deno?

Deno ships TypeScript support out of the box with no configuration, enforces a permissions-first security model so scripts can't silently read files or make network requests, and uses standard browser-compatible APIs wherever possible. It's built in Rust with Tokio for async I/O, making it fast and memory-safe at the core.

What is Deno and How It Differs from Node.js

Deno is a modern runtime for JavaScript and TypeScript built on V8 (the same engine Chrome and Node.js use), but with a completely different architecture underneath. While Node.js is written in C++ and uses libuv for async I/O, Deno is written in Rust and uses the Tokio async runtime. This gives Deno better memory safety guarantees and a more modern foundation.

At its core, Deno was designed around three principles:

JS / TS Code your script Deno Runtime TypeScript Compiler Permissions Check Web Standard APIs Rust + Tokio V8 Engine Executes JavaScript OS / System File, Network, Env Permissions Gate execute system calls

Deno's architecture: code passes through the runtime's permission gate before any system access

Installation and First Steps

Installing Deno is refreshingly simple — it's a single binary with no dependencies. No need for a package.json, no node_modules folder, no separate TypeScript compiler to install.

1

Install Deno

Install using the official shell script, Homebrew, Winget, or Chocolatey depending on your platform.

# Install via shell script
curl -fsSL https://deno.land/install.sh | sh

# Add to your shell profile (~/.zshrc or ~/.bashrc)
export DENO_INSTALL="$HOME/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"

# Verify installation
deno --version
# deno 2.x.x
# v8 12.x.x
# typescript 5.x.x
# PowerShell (with winget)
winget install DenoLand.Deno

# Or using PowerShell installer
irm https://deno.land/install.ps1 | iex

# Verify
deno --version
# macOS with Homebrew
brew install deno

# Upgrade to latest
brew upgrade deno

# Verify
deno --version
2

Run Your First Script

Deno runs JavaScript and TypeScript files directly — no compilation step needed.

hello.ts
// TypeScript works natively — no tsconfig.json required
const greet = (name: string): string => {
    return `Hello, ${name}! Welcome to Deno.`;
};

console.log(greet("World"));

// Use modern browser APIs
const response = await fetch("https://api.github.com/users/denoland");
const data = await response.json();
console.log(`Deno has ${data.public_repos} public repos on GitHub.`);
Terminal
# Run the script (--allow-net is required for fetch)
deno run --allow-net hello.ts

# Hello, World! Welcome to Deno.
# Deno has 42 public repos on GitHub.
3

Explore the REPL

Deno ships with an interactive REPL (Read-Eval-Print Loop) for quick experiments.

Terminal — REPL
$ deno

Deno 2.x.x
exit using ctrl+d, ctrl+c, or close()
> const x: number[] = [1, 2, 3];
undefined
> x.map(n => n * 2)
[ 2, 4, 6 ]
> Deno.version
{ deno: "2.x.x", v8: "12.x.x", typescript: "5.x.x" }

Security by Default

This is arguably Deno's most important feature. By default, a Deno script has zero access to the file system, network, environment variables, subprocess spawning, or high-resolution timers. You must explicitly grant each type of access when running a script. This means a malicious dependency can't silently exfiltrate your SSH keys or phone home to a remote server.

The Permissions System

Permissions are granted via flags when running a script. You can scope them broadly or narrowly:

Permission Flags
# Allow all network access
deno run --allow-net script.ts

# Allow network access to specific hosts only
deno run --allow-net=api.example.com,cdn.example.com script.ts

# Allow reading from a specific directory
deno run --allow-read=/home/user/data script.ts

# Allow writing to a specific directory
deno run --allow-write=/tmp script.ts

# Allow reading environment variables
deno run --allow-env=DATABASE_URL,SECRET_KEY script.ts

# Allow running subprocesses (specific commands)
deno run --allow-run=git,npm script.ts

# Allow all permissions (not recommended for production)
deno run --allow-all script.ts
# equivalent shorthand:
deno run -A script.ts
Least Privilege Principle

Always grant the minimum permissions your script actually needs. Avoid --allow-all in production. If a dependency is compromised, minimal permissions drastically limit the blast radius of an attack.

What Happens Without Permission

permissions-demo.ts
// This will throw if --allow-read is not granted
try {
    const text = await Deno.readTextFile("/etc/passwd");
    console.log(text);
} catch (err) {
    if (err instanceof Deno.errors.PermissionDenied) {
        console.error("Permission denied: --allow-read was not granted");
    }
}

// You can also check permissions programmatically at runtime
const status = await Deno.permissions.query({
    name: "read",
    path: "/etc/passwd"
});
console.log(status.state); // "denied" or "granted" or "prompt"

// Request permission interactively at runtime
const result = await Deno.permissions.request({ name: "net" });
if (result.state === "granted") {
    // safe to make network calls
}
Permission Flag What It Allows Scope Example
--allow-read Read files from disk --allow-read=./data
--allow-write Write files to disk --allow-write=/tmp
--allow-net Make network requests --allow-net=api.stripe.com
--allow-env Access environment variables --allow-env=PORT,DB_URL
--allow-run Spawn subprocesses --allow-run=git
--allow-sys Access system info --allow-sys=hostname
--allow-hrtime High-resolution timers No scoping available
--allow-ffi Call native libraries --allow-ffi=./lib.so

TypeScript as a First-Class Citizen

In Node.js, using TypeScript requires installing typescript as a dependency, creating a tsconfig.json, running tsc to compile, and often using ts-node or tsx for development. Deno eliminates all of that overhead — TypeScript is compiled on the fly using a built-in SWC-based transpiler.

Type Checking on Demand

typed-example.ts
interface User {
    id: number;
    name: string;
    email: string;
    role: "admin" | "user" | "guest";
}

async function fetchUser(id: number): Promise {
    const res = await fetch(`https://api.example.com/users/${id}`);
    if (!res.ok) {
        throw new Error(`HTTP ${res.status}: ${res.statusText}`);
    }
    return res.json() as Promise;
}

// Generic utility function with TypeScript generics
function groupBy(items: T[], key: keyof T): Record {
    return items.reduce((acc, item) => {
        const groupKey = String(item[key]);
        acc[groupKey] = acc[groupKey] ?? [];
        acc[groupKey].push(item);
        return acc;
    }, {} as Record);
}

const users: User[] = [
    { id: 1, name: "Alice", email: "alice@example.com", role: "admin" },
    { id: 2, name: "Bob", email: "bob@example.com", role: "user" },
    { id: 3, name: "Carol", email: "carol@example.com", role: "user" },
];

const byRole = groupBy(users, "role");
console.log(byRole);
Terminal
# Run without type checking (fast, uses transpilation only)
deno run typed-example.ts

# Run WITH full type checking (catches type errors)
deno run --check typed-example.ts

# Type check without running
deno check typed-example.ts

# Format code (built-in formatter, like Prettier)
deno fmt typed-example.ts

# Lint code (built-in linter)
deno lint typed-example.ts
Performance Note

Deno caches compiled TypeScript aggressively. The first run compiles and caches; subsequent runs skip compilation entirely. You get TypeScript's safety without paying the compilation cost on every run after the first.

Modern Module System

Node.js uses CommonJS (require) and npm for package management. Deno takes a completely different approach: it uses ESM (ES Modules) exclusively and imports directly from URLs — like browsers do. There is no node_modules folder and no package.json by default.

URL Imports and JSR

modules-demo.ts
// Import from JSR (Deno's modern package registry)
import { assertEquals } from "jsr:@std/assert";
import { join } from "jsr:@std/path";

// Import from npm (Deno has full npm compatibility)
import express from "npm:express@4";
import lodash from "npm:lodash";

// Import from a URL directly (older style, still works)
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";

// Local imports use explicit extensions
import { helper } from "./utils.ts";
import type { Config } from "./types.ts";

// Dynamic imports work too
const module = await import("./heavy-module.ts");

Import Maps for Cleaner Imports

Import maps let you define aliases so you don't repeat full URLs everywhere. This is the recommended approach for larger projects.

deno.json
{
  "imports": {
    "@std/assert": "jsr:@std/assert@^1.0.0",
    "@std/path": "jsr:@std/path@^1.0.0",
    "@std/fs": "jsr:@std/fs@^1.0.0",
    "zod": "npm:zod@^3.22.0",
    "hono": "npm:hono@^4.0.0"
  },
  "tasks": {
    "dev": "deno run --watch --allow-net --allow-read main.ts",
    "start": "deno run --allow-net --allow-read main.ts",
    "test": "deno test --allow-read",
    "check": "deno check main.ts",
    "lint": "deno lint",
    "fmt": "deno fmt"
  },
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true
  }
}
main.ts — after setting up deno.json
// Clean imports using your deno.json aliases
import { assertEquals } from "@std/assert";
import { join } from "@std/path";
import { z } from "zod";

// Input validation with Zod
const UserSchema = z.object({
    name: z.string().min(2).max(100),
    email: z.string().email(),
    age: z.number().int().min(0).max(150),
});

type User = z.infer;

const result = UserSchema.safeParse({
    name: "Alice",
    email: "alice@example.com",
    age: 30,
});

if (result.success) {
    const user: User = result.data;
    console.log("Valid user:", user);
} else {
    console.error("Validation failed:", result.error.issues);
}

Building an HTTP Server

Deno 1.9 introduced Deno.serve() — a stable, high-performance built-in HTTP server. It uses the same Request/Response API as the browser's Fetch API, making it immediately familiar to frontend developers.

Client Browser / curl Deno.serve() Request (Web API) Handler function Response (Web API) Your Handler Route logic DB / file / API calls here Response JSON / HTML / etc

Deno's built-in HTTP server uses the standard Web Fetch API for requests and responses

server.ts — REST API with Deno.serve()
// In-memory "database" for demo purposes
interface Post {
    id: number;
    title: string;
    body: string;
    author: string;
    createdAt: string;
}

const posts: Post[] = [
    { id: 1, title: "Hello Deno", body: "My first post", author: "Alice", createdAt: "2026-01-01" },
];
let nextId = 2;

// Helper to create JSON responses
function json(data: unknown, status = 200): Response {
    return new Response(JSON.stringify(data), {
        status,
        headers: {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "*",
        },
    });
}

// Route handler
function handler(req: Request): Response | Promise {
    const url = new URL(req.url);
    const path = url.pathname;
    const method = req.method;

    // GET /posts — list all posts
    if (method === "GET" && path === "/posts") {
        return json(posts);
    }

    // GET /posts/:id — get single post
    const postMatch = path.match(/^\/posts\/(\d+)$/);
    if (method === "GET" && postMatch) {
        const post = posts.find(p => p.id === Number(postMatch[1]));
        return post ? json(post) : json({ error: "Not found" }, 404);
    }

    // POST /posts — create a post
    if (method === "POST" && path === "/posts") {
        return req.json().then((body: Omit) => {
            const post: Post = {
                id: nextId++,
                title: body.title,
                body: body.body,
                author: body.author,
                createdAt: new Date().toISOString().split("T")[0],
            };
            posts.push(post);
            return json(post, 201);
        });
    }

    // DELETE /posts/:id
    if (method === "DELETE" && postMatch) {
        const id = Number(postMatch[1]);
        const index = posts.findIndex(p => p.id === id);
        if (index === -1) return json({ error: "Not found" }, 404);
        posts.splice(index, 1);
        return json({ message: "Deleted" });
    }

    return json({ error: "Route not found" }, 404);
}

// Start the server
Deno.serve({ port: 8000 }, handler);
console.log("Server running at http://localhost:8000");
Terminal — run the server
# --watch restarts automatically on file changes (like nodemon)
deno run --watch --allow-net server.ts

# Test the API
curl http://localhost:8000/posts
curl -X POST http://localhost:8000/posts \
  -H "Content-Type: application/json" \
  -d '{"title":"My Post","body":"Hello!","author":"Bob"}'
curl http://localhost:8000/posts/1
curl -X DELETE http://localhost:8000/posts/1

File System Operations

file-ops.ts
// Reading a file
const text = await Deno.readTextFile("./data.txt");
console.log(text);

// Writing a file
await Deno.writeTextFile("./output.txt", "Hello from Deno!\n");

// Appending to a file
await Deno.writeTextFile("./log.txt", `${new Date().toISOString()}: event\n`, {
    append: true,
});

// Reading a directory
for await (const entry of Deno.readDir(".")) {
    if (entry.isFile) {
        console.log(`File: ${entry.name}`);
    } else if (entry.isDirectory) {
        console.log(`Dir:  ${entry.name}/`);
    }
}

// Check if a file exists
try {
    const stat = await Deno.stat("./config.json");
    console.log(`File size: ${stat.size} bytes`);
} catch {
    console.log("config.json does not exist");
}

// Read JSON files directly
const config = JSON.parse(await Deno.readTextFile("./deno.json"));
console.log(config.tasks);

Deno vs Node.js: Side-by-Side

Understanding where Deno differs from Node.js helps you decide when to use each. They're not enemies — Node.js is battle-tested with a huge ecosystem; Deno is modern, secure, and TypeScript-native. Choose based on your constraints.

Feature Node.js Deno
Security Full OS access by default Sandboxed; explicit permissions required
TypeScript Needs tsc + ts-node or tsx Built-in, zero config
Module system CommonJS + ESM (mixed) ESM only (browser-compatible)
Package manager npm / yarn / pnpm URL imports + JSR + npm compat
package.json Required Optional (deno.json instead)
node_modules Required (can be huge) Not needed by default (global cache)
Standard library Minimal built-ins Rich @std library via JSR
Built-in tools None (use ESLint, Prettier, etc.) Formatter, linter, test runner built-in
Web APIs Limited (fetch added in v18) Full Web API compatibility
Ecosystem maturity Massive (npm has ~2M packages) Growing; npm compatibility fills gaps
Core language C++ + libuv Rust + Tokio (memory-safe)
Deployment Node-compatible hosts everywhere Deno Deploy, Cloudflare Workers, Fly.io

Built-in Testing

Deno ships with a test runner so you don't need Jest, Mocha, or Vitest. Write tests directly alongside your code and run them with deno test.

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

// Simple unit test
Deno.test("add two numbers", () => {
    assertEquals(1 + 2, 3);
});

// Async test
Deno.test("fetch returns 200", async () => {
    const res = await fetch("https://httpbin.org/status/200");
    assertEquals(res.status, 200);
});

// Test with sub-steps
Deno.test("user validation", async (t) => {
    await t.step("valid email passes", () => {
        const email = "test@example.com";
        assertEquals(email.includes("@"), true);
    });

    await t.step("invalid email fails", () => {
        const email = "not-an-email";
        assertEquals(email.includes("@"), false);
    });
});

// Snapshot testing
Deno.test("snapshot test", async (t) => {
    const data = { name: "Alice", role: "admin" };
    await assertSnapshot(t, data);
});
Terminal
# Run all tests in current directory
deno test

# Run tests in a specific file
deno test math_test.ts

# Run with network permission (for async fetch tests)
deno test --allow-net

# Watch mode — re-runs on file changes
deno test --watch

# Generate coverage report
deno test --coverage=./coverage
deno coverage ./coverage

Deno Deploy: Edge Deployment

Deno Deploy is a globally distributed serverless platform that runs your Deno scripts at the edge — in data centers close to your users. It's similar to Cloudflare Workers but built specifically for Deno. Deployments happen in seconds: push to GitHub and your code is live globally.

edge-function.ts — runs on Deno Deploy
// This exact file can be deployed to Deno Deploy globally
Deno.serve(async (req: Request) => {
    const url = new URL(req.url);

    // Serve a dynamic greeting based on query param
    if (url.pathname === "/greet") {
        const name = url.searchParams.get("name") ?? "World";
        return new Response(`Hello, ${name}! From the edge.`, {
            headers: { "Content-Type": "text/plain" },
        });
    }

    // Proxy an external API with caching
    if (url.pathname === "/github") {
        const res = await fetch("https://api.github.com/repos/denoland/deno", {
            headers: { "User-Agent": "Deno Deploy" },
        });
        const data = await res.json();
        return Response.json({
            stars: data.stargazers_count,
            forks: data.forks_count,
            description: data.description,
        });
    }

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

Deno CLI Commands Quick Reference

Command Description
deno run file.ts Run a script
deno run --watch file.ts Run with auto-reload on changes
deno check file.ts Type-check without running
deno test Run all tests in current directory
deno fmt Format code (built-in Prettier-like)
deno lint Lint code for common errors
deno doc file.ts Generate documentation from JSDoc
deno compile file.ts Compile to a standalone executable
deno bundle file.ts Bundle script and dependencies
deno info file.ts Show dependency tree
deno cache file.ts Pre-cache dependencies
deno upgrade Upgrade Deno to latest version

When to Choose Deno

Deno isn't a replacement for Node.js in every context — it's a thoughtfully redesigned runtime that shines in specific scenarios. Here's a practical guide for making the decision:

Choose Deno When...

  • Security matters most: Running untrusted code or scripts with access to sensitive files
  • TypeScript is your default: No more compilation setup — just write .ts files
  • Building edge functions: Deno Deploy gives sub-10ms cold starts globally
  • Starting fresh: New projects benefit from Deno's clean, modern defaults
  • Tooling fatigue: Built-in formatter, linter, and test runner reduce setup overhead
  • Browser code sharing: Code that works in Deno often works in browsers without modification

Stick with Node.js When...

  • Ecosystem depth is critical: You rely on npm packages that don't have Deno equivalents
  • Team expertise: Your team knows Node.js deeply and migration costs are high
  • Legacy codebase: Existing Node.js applications don't need to migrate
  • Hosting constraints: Your deployment target only supports Node.js
  • Framework choice: You want Express, Fastify, or NestJS exactly as-is
"The goal with Deno is not to replace Node.js, but to provide a better environment for server-side JavaScript going forward — one that doesn't carry the technical debt of the past."
— Ryan Dahl, Creator of Deno

Deno 2.0 brought full npm compatibility, meaning you can now use the vast majority of npm packages in Deno without any friction. The gap between Deno and Node.js has narrowed significantly, and Deno's unique strengths — security, TypeScript-first, Web APIs, zero-config tooling — make it a compelling choice for modern server-side JavaScript development. Whether you adopt it for new projects, edge deployments, or scripting, Deno rewards developers who value clean defaults and modern design principles.

Deno JavaScript TypeScript Runtime Security Node.js Backend
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and Node.js.