MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Introduction to Deno: A Secure JavaScript Runtime

Explore Deno: security, TypeScript support, modern APIs, and how it compares to Node.js.

DenoTypeScriptJavaScriptRuntime

By MinhVo

Introduction

When Ryan Dahl, the original creator of Node.js, gave his famous talk "10 Things I Regret About Node.js" in 2018, he outlined fundamental design decisions that had become pain points for the JavaScript community. Security was his biggest regret—Node.js scripts have unrestricted access to the file system, network, and environment variables by default. From this retrospective, Deno was born: a secure-by-default JavaScript and TypeScript runtime built on V8 and Rust that addresses Node.js's architectural shortcomings while embracing modern web standards.

Deno's security model is its defining feature. Every script runs in a sandbox with zero permissions. To read files, make network requests, access environment variables, or spawn processes, you must explicitly grant permission via command-line flags. This is analogous to how mobile apps request permissions—each capability is opt-in, not opt-out. In an era of supply chain attacks where malicious npm packages have compromised millions of computers, this approach provides critical defense-in-depth.

Beyond security, Deno offers native TypeScript support without configuration, a comprehensive standard library, web-standard APIs (fetch, WebSocket, Web Streams), built-in tooling (formatter, linter, test runner), and single-binary compilation. This guide explores Deno's architecture, security model, practical applications, and how it compares to Node.js for modern development.

JavaScript runtime security architecture

Understanding Deno: Security and Architecture

Deno's architecture is built on two foundational technologies: V8 (Google's JavaScript engine) for executing JavaScript, and Rust for the runtime's I/O layer, file system access, and security enforcement. This combination provides the performance of V8 with Rust's memory safety guarantees.

The Permission System

Deno's permission system defines seven categories of access:

  • --allow-read — Read file system access (optionally scoped to specific paths)
  • --allow-write — Write file system access
  • --allow-net — Network access (optionally scoped to specific hosts/ports)
  • --allow-env — Environment variable access
  • --allow-run — Spawn child processes
  • --allow-ffi — Foreign Function Interface access
  • --allow-hrtime — High-resolution time measurement

Each permission can be further scoped: --allow-read=./data only allows reading the ./data directory, and --allow-net=api.example.com only allows connections to that specific host. This fine-grained control means you can grant exactly the access your application needs and nothing more.

Module System

Deno uses ES modules exclusively—there's no CommonJS require(). Modules are loaded via URLs, file paths, or import maps. This design decision eliminates the node_modules directory entirely and makes dependency trees explicit. You import directly from URLs:

import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
import { oak } from "https://deno.land/x/oak/mod.ts";

This approach has both advantages (explicit dependencies, no phantom dependencies) and challenges (URL imports can be verbose). Import maps solve the verbosity problem by mapping short names to URLs.

TypeScript Without Configuration

Deno natively executes TypeScript files. When you run deno run app.ts, the runtime uses SWC to transpile TypeScript to JavaScript on the fly. Type checking is separate—you run deno check app.ts to perform full TypeScript type analysis. This separation means you get fast execution (no type checking overhead) and thorough type checking when you want it.

Modern development tools and TypeScript

Architecture and Design Patterns

The Standard Library

Deno's standard library (deno.land/std) provides modules for common tasks: file system operations (fs), path manipulation (path), HTTP server (http), testing (assert), encoding (encoding), and more. These modules follow web standards where applicable and provide consistent APIs across the runtime.

Import Maps for Dependency Management

Import maps replace package.json for dependency management:

// import_map.json
{
  "imports": {
    "std/": "https://deno.land/std@0.208.0/",
    "oak": "https://deno.land/x/oak@v12.6.1/mod.ts",
    "postgres": "https://deno.land/x/postgres/mod.ts",
    "djwt": "https://deno.land/x/djwt/mod.ts"
  }
}

Fresh: Deno's Web Framework

Fresh is Deno's official web framework, built on Deno and Preact. It uses server-side rendering by default with islands of interactivity, similar to Astro but purpose-built for Deno's runtime.

Step-by-Step Implementation

Let's build practical applications demonstrating Deno's security model and capabilities.

HTTP Server with Explicit Permissions

// server.ts
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
 
const PORT = parseInt(Deno.env.get("PORT") || "8000");
 
async function handler(request: Request): Promise<Response> {
  const url = new URL(request.url);
  const method = request.method;
 
  if (url.pathname === "/" && method === "GET") {
    return new Response(
      JSON.stringify({
        message: "Welcome to the Deno API",
        version: "1.0.0",
        runtime: "Deno",
        timestamp: new Date().toISOString(),
      }),
      {
        headers: { "Content-Type": "application/json" },
      }
    );
  }
 
  if (url.pathname === "/health" && method === "GET") {
    return new Response(
      JSON.stringify({ status: "healthy", uptime: performance.now() }),
      { headers: { "Content-Type": "application/json" } }
    );
  }
 
  if (url.pathname === "/file" && method === "GET") {
    try {
      // This will FAIL without --allow-read permission
      const content = await Deno.readTextFile("./data/config.json");
      return new Response(content, {
        headers: { "Content-Type": "application/json" },
      });
    } catch {
      return new Response(
        JSON.stringify({ error: "File not found or permission denied" }),
        { status: 404, headers: { "Content-Type": "application/json" } }
      );
    }
  }
 
  return new Response(
    JSON.stringify({ error: "Not found" }),
    { status: 404, headers: { "Content-Type": "application/json" } }
  );
}
 
// Run with: deno run --allow-net=:8000 --allow-read=./data server.ts
serve(handler, { port: PORT });

Building a Secure Middleware Pipeline

// middleware.ts
type Handler = (req: Request) => Promise<Response> | Response;
type Middleware = (req: Request, next: Handler) => Promise<Response> | Response;
 
function compose(...middlewares: Middleware[]): Handler {
  return async (req: Request): Promise<Response> => {
    let index = -1;
 
    async function dispatch(i: number): Promise<Response> {
      if (i <= index) throw new Error("next() called multiple times");
      index = i;
      const fn = middlewares[i];
      if (!fn) return new Response("Not found", { status: 404 });
      return fn(req, () => dispatch(i + 1));
    }
 
    return dispatch(0);
  };
}
 
// Rate limiting middleware
const rateLimitMap = new Map<string, { count: number; resetTime: number }>();
 
const rateLimit: Middleware = async (req, next) => {
  const ip = req.headers.get("x-forwarded-for") || "unknown";
  const now = Date.now();
  const limit = rateLimitMap.get(ip);
 
  if (!limit || now > limit.resetTime) {
    rateLimitMap.set(ip, { count: 1, resetTime: now + 60000 });
  } else if (limit.count >= 100) {
    return new Response(
      JSON.stringify({ error: "Rate limit exceeded" }),
      { status: 429, headers: { "Content-Type": "application/json" } }
    );
  } else {
    limit.count++;
  }
 
  return next(req);
};
 
// CORS middleware
const cors: Middleware = async (req, next) => {
  if (req.method === "OPTIONS") {
    return new Response(null, {
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
        "Access-Control-Allow-Headers": "Content-Type, Authorization",
      },
    });
  }
 
  const response = await next(req);
  response.headers.set("Access-Control-Allow-Origin", "*");
  return response;
};
 
// Logging middleware
const logger: Middleware = async (req, next) => {
  const start = performance.now();
  const response = await next(req);
  const duration = performance.now() - start;
  console.log(`${req.method} ${new URL(req.url).pathname} - ${response.status} (${duration.toFixed(2)}ms)`);
  return response;
};
 
const app = compose(logger, cors, rateLimit, handler);

File System Operations with Scoped Permissions

// file-processor.ts
// Run with: deno run --allow-read=./input --allow-write=./output file-processor.ts
 
import { join } from "https://deno.land/std@0.208.0/path/mod.ts";
import { walk } from "https://deno.land/std@0.208.0/fs/walk.ts";
 
async function processFiles(inputDir: string, outputDir: string) {
  // Ensure output directory exists
  await Deno.mkdir(outputDir, { recursive: true });
 
  for await (const entry of walk(inputDir, { exts: [".json"] })) {
    const content = await Deno.readTextFile(entry.path);
    const data = JSON.parse(content);
 
    // Process data
    const processed = {
      ...data,
      processedAt: new Date().toISOString(),
      recordCount: Array.isArray(data.records) ? data.records.length : 0,
    };
 
    const outputPath = join(outputDir, entry.name);
    await Deno.writeTextFile(outputPath, JSON.stringify(processed, null, 2));
    console.log(`Processed: ${entry.name} -> ${outputPath}`);
  }
}
 
await processFiles("./input", "./output");

JWT Authentication

// auth.ts
import { create, verify, getNumericDate } from "https://deno.land/x/djwt/mod.ts";
 
const JWT_SECRET = await crypto.subtle.generateKey(
  { name: "HMAC", hash: "SHA-256" },
  true,
  ["sign", "verify"]
);
 
interface UserClaims {
  sub: string;
  email: string;
  role: string;
}
 
async function generateToken(claims: UserClaims): Promise<string> {
  return await create(
    { alg: "HS256", typ: "JWT" },
    {
      ...claims,
      exp: getNumericDate(60 * 60), // 1 hour
      iat: getNumericDate(0),
    },
    JWT_SECRET
  );
}
 
async function verifyToken(token: string): Promise<UserClaims> {
  try {
    const payload = await verify(token, JWT_SECRET);
    return payload as unknown as UserClaims;
  } catch {
    throw new Error("Invalid or expired token");
  }
}
 
// Middleware for protected routes
const authMiddleware: Middleware = async (req, next) => {
  const authHeader = req.headers.get("Authorization");
  if (!authHeader?.startsWith("Bearer ")) {
    return new Response(
      JSON.stringify({ error: "Missing or invalid Authorization header" }),
      { status: 401, headers: { "Content-Type": "application/json" } }
    );
  }
 
  try {
    const token = authHeader.slice(7);
    const claims = await verifyToken(token);
 
    // Attach user to request (using a header for simplicity)
    const headers = new Headers(req.headers);
    headers.set("X-User-Id", claims.sub);
    headers.set("X-User-Role", claims.role);
 
    return next(new Request(req.url, { method: req.method, headers, body: req.body }));
  } catch {
    return new Response(
      JSON.stringify({ error: "Invalid token" }),
      { status: 403, headers: { "Content-Type": "application/json" } }
    );
  }
};

Secure coding practices and authentication

Real-World Use Cases

Use Case 1: Secure Script Execution

Organizations use Deno to run third-party scripts safely. Instead of granting full system access to every script, Deno's sandbox limits what each script can do. This is particularly valuable for CI/CD pipelines, data processing scripts, and plugin systems where untrusted code executes on your infrastructure.

Use Case 2: Edge Computing

Deno Deploy runs Deno code at the edge in 35+ global regions. Companies like Netlify and Supabase use Deno for their edge function platforms. The fast startup time and small memory footprint make Deno ideal for serverless edge computing where cold starts matter.

Use Case 3: API Development with Fresh

Fresh, Deno's official web framework, combines server-side rendering with islands architecture for fast, SEO-friendly web applications. It's used by Deno's own documentation site and various production applications requiring excellent performance and developer experience.

Use Case 4: CLI Tools and Automation

Deno's single-binary compilation makes it excellent for distributing CLI tools. You can compile a Deno application into a standalone executable that requires no runtime installation. Tools like deployctl (for Deno Deploy) and various community CLIs demonstrate this pattern.

Best Practices for Production

  1. Grant minimal permissions: Use the most restrictive permission flags possible. Prefer --allow-read=./specific-dir over --allow-read. Use --allow-net=hostname:port instead of --allow-net.

  2. Use import maps for dependency management: Create an import_map.json to centralize dependency URLs. This makes upgrading dependencies easier and import statements cleaner.

  3. Pin dependency versions: Always pin exact versions in import maps. Avoid using @latest or unversioned URLs in production.

  4. Enable TypeScript strict mode: Configure compilerOptions.strict: true in deno.json. Deno's TypeScript support is excellent—leverage it fully.

  5. Use the built-in test runner: Deno's deno test runner supports async tests, mocking, and code coverage. Use it instead of external frameworks.

  6. Run deno lint and deno fmt in CI: Deno's built-in linter catches common bugs, and the formatter ensures consistent code style. Add these to your CI pipeline.

  7. Use deno compile for distribution: Compile your application to a standalone binary for easy deployment and distribution. This eliminates Deno installation on target machines.

  8. Handle permission errors gracefully: Wrap permission-sensitive operations in try-catch blocks. When a permission is denied, provide helpful error messages indicating which flag is needed.

Common Pitfalls and Solutions

PitfallImpactSolution
Using --allow-all everywhereNo security benefit, defeats Deno's purposeGrant specific permissions per use case
Not pinning dependency versionsUnexpected breaking changesUse exact versions in import maps
Assuming Node.js API compatibilityRuntime errors for fs, path, processUse Deno equivalents (Deno.readTextFile, import from "std/path")
Ignoring TypeScript errorsBugs in productionRun deno check in CI before deploying
Using CommonJS modulesImport errorsUse ES modules exclusively; for CJS compatibility, use npm: specifier

Performance Optimization

// Performance optimization patterns in Deno
 
// 1. Use Web Workers for CPU-intensive tasks
const workerPool = Array.from({ length: navigator.hardwareConcurrency }, () =>
  new Worker(new URL("./worker.ts", import.meta.url).href, { type: "module" })
);
 
// 2. Stream large files instead of loading into memory
async function streamFile(path: string): Response {
  const file = await Deno.open(path, { read: true });
  return new Response(file.readable, {
    headers: { "Content-Type": "application/octet-stream" },
  });
}
 
// 3. Connection pooling for database connections
import { Pool } from "https://deno.land/x/postgres/mod.ts";
 
const pool = new Pool({
  hostname: "localhost",
  database: "myapp",
  port: 5432,
  max: 20, // Pool size
}, 10);
 
async function query(sql: string, params: unknown[] = []) {
  const client = await pool.connect();
  try {
    return await client.queryObject(sql, params);
  } finally {
    client.release();
  }
}
 
// 4. Use caching for expensive computations
const memoCache = new Map<string, { value: unknown; expiry: number }>();
 
function memoize<T>(key: string, ttl: number, fn: () => T): T {
  const cached = memoCache.get(key);
  if (cached && cached.expiry > Date.now()) return cached.value as T;
  const value = fn();
  memoCache.set(key, { value, expiry: Date.now() + ttl });
  return value;
}

Comparison with Alternatives

FeatureDenoNode.jsBunCloudflare Workers
Security modelSandbox (opt-in permissions)Full accessFull accessIsolate-based
TypeScriptNativeRequires tsx/ts-nodeNativeVia Wrangler
Module systemES modules onlyCJS + ESMESM + CJSES modules
Built-in toolingFormatter, linter, test, benchNoneBundler, test, package managerWrangler CLI
npm compatibilityVia npm: specifierNativeNativeVia Wrangler
Standard libraryComprehensive (deno.land/std)MinimalMinimalWorkers API
Binary compilationYes (deno compile)Yes (SEA)YesVia Wrangler
Global edge deployDeno DeployLimitedFly.ioCloudflare
MaturityGrowingBattle-testedRapidly growingProduction-ready

Deno is ideal when security and TypeScript-first development are priorities. Node.js is the safe choice for maximum ecosystem compatibility. Bun offers the best raw performance. Cloudflare Workers provides the most mature edge computing platform.

Advanced Patterns and Techniques

Subhosting: Running Untrusted Code Safely

// Deno Subhosting allows you to run user-provided code in isolated V8 isolates
// Each user's code runs in its own sandbox with configurable permissions
 
import { createRunner } from "https://deno.land/x/subhosting/mod.ts";
 
const runner = createRunner({
  permissions: {
    net: false,      // No network access
    read: false,     // No file system access
    write: false,
    env: false,
  },
  memoryLimit: 128 * 1024 * 1024, // 128MB
  timeout: 5000,                   // 5 seconds
});
 
// Run user-provided code safely
const result = await runner.run(`
  export default function handler(request) {
    return new Response(JSON.stringify({ hello: "world" }), {
      headers: { "Content-Type": "application/json" }
    });
  }
`, new Request("http://localhost/"));
 
console.log(result.response);

Event-Driven Architecture with BroadcastChannel

// Cross-worker communication using BroadcastChannel
const channel = new BroadcastChannel("app-events");
 
// Worker 1: Order service
channel.postMessage({
  type: "ORDER_CREATED",
  payload: { orderId: "123", userId: "user-1", total: 99.99 },
});
 
// Worker 2: Notification service
channel.onmessage = (event) => {
  if (event.data.type === "ORDER_CREATED") {
    sendEmail(event.data.payload.userId, "Order confirmed!");
  }
};
 
// Worker 3: Analytics service
channel.onmessage = (event) => {
  if (event.data.type === "ORDER_CREATED") {
    trackEvent("purchase", event.data.payload);
  }
};

Testing Strategies

import { assertEquals, assertThrows, assertRejects } from "https://deno.land/std/assert/mod.ts";
import { spy, assertSpyCalls } from "https://deno.land/std/testing/mock.ts";
 
Deno.test("generateToken creates valid JWT", async () => {
  const token = await generateToken({
    sub: "user-123",
    email: "test@example.com",
    role: "admin",
  });
 
  assertEquals(typeof token, "string");
  assertEquals(token.split(".").length, 3); // JWT has 3 parts
});
 
Deno.test("verifyToken rejects expired tokens", async () => {
  // Create a token with past expiration
  const expiredToken = await create(
    { alg: "HS256", typ: "JWT" },
    { sub: "user-123", exp: getNumericDate(-3600) },
    JWT_SECRET
  );
 
  await assertRejects(
    () => verifyToken(expiredToken),
    Error,
    "Invalid or expired token"
  );
});
 
Deno.test("rate limiter blocks excessive requests", async () => {
  const mockHandler = spy(() => new Response("OK"));
  const mockNext = spy(() => new Response("OK"));
 
  // Send 101 requests (limit is 100)
  for (let i = 0; i < 101; i++) {
    const req = new Request("http://localhost/test", {
      headers: { "x-forwarded-for": "192.168.1.1" },
    });
    await rateLimit(req, mockNext);
  }
 
  assertSpyCalls(mockNext, 100); // Last request blocked
});
 
// Integration tests
Deno.test({
  name: "API endpoints return correct responses",
  permissions: { net: true, read: true, write: true },
  fn: async () => {
    const server = Deno.listen({ port: 8001 });
    // ... start server and test endpoints
    server.close();
  },
});

Future Outlook

Deno continues to evolve rapidly. Deno 2.0 brought full npm compatibility, eliminating the biggest adoption barrier. JSR (jsr.io) provides a modern package registry designed for TypeScript-first modules. Deno Deploy offers global edge deployment with Deno KV for distributed state.

The broader JavaScript ecosystem is moving toward Deno's original vision: TypeScript is now the dominant language for new projects, web standards (fetch, WebSocket) are universally supported, and security concerns around npm packages are driving interest in sandboxed runtimes. Deno's permission model, while requiring more upfront configuration, provides genuine security benefits that will become increasingly important as supply chain attacks grow.

Conclusion

Deno represents a thoughtful reimagining of server-side JavaScript, addressing real security and developer experience issues in the Node.js ecosystem. Its permission system provides defense-in-depth against malicious code, native TypeScript support eliminates configuration overhead, and web-standard APIs ensure code portability.

The key takeaways are: Deno's sandbox model requires explicit permission grants for file, network, and environment access, preventing unauthorized code from accessing sensitive resources. TypeScript runs natively without configuration—write .ts files and execute them directly. Web-standard APIs (fetch, WebSocket, streams) make code portable across Deno, browsers, and other runtimes.

Start by installing Deno and running a simple script without any permissions to see the sandbox in action. Gradually add permissions as needed and observe how Deno protects you from unexpected access. The Deno documentation at docs.deno.com and the standard library at deno.land/std provide comprehensive resources for building secure, modern applications.