Deno
Deno is a secure runtime for JavaScript and TypeScript created by Ryan Dahl, the original creator of Node.js. Released in 2020, Deno was built to address the design mistakes Dahl identified in Node.js after a decade of experience. Deno emphasizes security, modern JavaScript, and developer experience while remaining compatible with Node.js packages.
What is Deno?
Deno is a secure-by-default JavaScript/TypeScript runtime:
// server.ts - TypeScript works natively!
Deno.serve((req: Request) => {
return new Response("Hello from Deno!");
});
console.log("Server running on http://localhost:8000");
Run with: deno run --allow-net server.ts
Key Features:
- Security-first: Explicit permissions required
- Native TypeScript: No configuration needed
- Web standard APIs: fetch, URL, etc. built-in
- No package.json: URL imports instead
- Single executable: Includes formatter, linter, test runner
- Node.js compatible: Can use npm packages
Why Deno?
1. Security by Default
Deno requires explicit permission for everything:
# ❌ This will fail - no permissions
deno run server.ts
# ✓ Grant network access
deno run --allow-net server.ts
# ✓ Grant file read access
deno run --allow-read server.ts
# ✓ Grant multiple permissions
deno run --allow-net --allow-read --allow-write server.ts
# ✓ Grant all permissions (like Node.js)
deno run -A server.ts
Permission types:
--allow-read
: File system read--allow-write
: File system write--allow-net
: Network access--allow-env
: Environment variables--allow-run
: Spawn subprocesses--allow-all
/-A
: All permissions
2. No package.json or node_modules
Import directly from URLs:
// Import from URL
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts";
// Import from npm (Deno 1.28+)
import express from "npm:express@4";
Advantages:
- No
node_modules
folder - Explicit versioning in imports
- Code is cached locally
- No package manager needed (but available)
3. Native TypeScript
Zero configuration needed:
// types.ts - just works!
interface User {
id: number;
name: string;
email: string;
}
export async function getUser(id: number): Promise<User> {
const response = await fetch(`https://api.example.com/users/${id}`);
return response.json();
}
const user = await getUser(1);
console.log(user.name);
Run: deno run --allow-net types.ts
4. Built-in Tools
Everything included:
# Format code
deno fmt
# Lint code
deno lint
# Run tests
deno test
# Bundle code
deno bundle
# Compile to executable
deno compile server.ts
# Install scripts globally
deno install --allow-net --allow-read https://deno.land/std/http/file_server.ts
# Documentation generator
deno doc
5. Web Standard APIs
Uses modern Web APIs:
// Fetch API (built-in)
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// Web Streams
const stream = new ReadableStream({
start(controller) {
controller.enqueue('Hello ');
controller.enqueue('World!');
controller.close();
},
});
// URL API
const url = new URL('https://example.com/path?query=value');
console.log(url.searchParams.get('query'));
// FormData
const formData = new FormData();
formData.append('name', 'Alice');
HTTP Server
Basic Server
Deno.serve((req: Request) => {
const url = new URL(req.url);
if (url.pathname === '/') {
return new Response('Home page');
}
if (url.pathname === '/api/users') {
return Response.json({ users: ['Alice', 'Bob'] });
}
return new Response('Not Found', { status: 404 });
});
Custom Port
Deno.serve({ port: 3000 }, (req) => {
return new Response('Hello on port 3000');
});
With Router
import { Router } from "https://deno.land/x/oak/mod.ts";
const router = new Router();
router
.get('/', (ctx) => {
ctx.response.body = 'Home';
})
.get('/users/:id', (ctx) => {
ctx.response.body = `User ${ctx.params.id}`;
})
.post('/users', async (ctx) => {
const body = await ctx.request.body().value;
ctx.response.body = { created: body };
});
File System
Reading Files
// Read as text
const text = await Deno.readTextFile('file.txt');
// Read as bytes
const data = await Deno.readFile('file.txt');
// Read directory
for await (const entry of Deno.readDir('./')) {
console.log(entry.name, entry.isFile, entry.isDirectory);
}
// File info
const fileInfo = await Deno.stat('file.txt');
console.log(fileInfo.size, fileInfo.mtime);
Writing Files
// Write text file
await Deno.writeTextFile('output.txt', 'Hello World');
// Write bytes
const encoder = new TextEncoder();
await Deno.writeFile('output.txt', encoder.encode('Hello'));
// Append to file
await Deno.writeTextFile('log.txt', 'New line\n', { append: true });
File Operations
// Copy file
await Deno.copyFile('source.txt', 'dest.txt');
// Remove file
await Deno.remove('file.txt');
// Remove directory (recursive)
await Deno.remove('directory', { recursive: true });
// Create directory
await Deno.mkdir('new-directory', { recursive: true });
URL Imports
Importing Modules
// Standard library
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { parse } from "https://deno.land/[email protected]/flags/mod.ts";
// Third-party modules (from deno.land/x)
import { Application } from "https://deno.land/x/[email protected]/mod.ts";
// From GitHub
import { foo } from "https://raw.githubusercontent.com/user/repo/main/mod.ts";
// From npm
import express from "npm:express@4";
import React from "npm:react@18";
Import Maps
Use import maps to simplify imports:
// import_map.json
{
"imports": {
"std/": "https://deno.land/[email protected]/",
"oak": "https://deno.land/x/[email protected]/mod.ts",
"react": "npm:react@18"
}
}
// Use short names
import { serve } from "std/http/server.ts";
import { Application } from "oak";
import React from "react";
Run with: deno run --import-map=import_map.json server.ts
deno.json Configuration
{
"imports": {
"std/": "https://deno.land/[email protected]/",
"@/": "./src/"
},
"tasks": {
"dev": "deno run --watch --allow-net server.ts",
"start": "deno run --allow-net server.ts"
},
"fmt": {
"useTabs": false,
"lineWidth": 100
},
"lint": {
"rules": {
"tags": ["recommended"]
}
}
}
Testing
Built-in test runner:
// math_test.ts
import { assertEquals, assertThrows } from "https://deno.land/[email protected]/testing/asserts.ts";
Deno.test("addition", () => {
assertEquals(2 + 2, 4);
});
Deno.test("subtraction", () => {
assertEquals(5 - 3, 2);
});
Deno.test("async test", async () => {
const result = await fetchData();
assertEquals(result.status, "ok");
});
Deno.test("error test", () => {
assertThrows(
() => {
throw new Error("Error!");
},
Error,
"Error!"
);
});
Run tests: deno test
Test Options
# Run specific test file
deno test math_test.ts
# Watch mode
deno test --watch
# Coverage
deno test --coverage=cov_profile
deno coverage cov_profile
# Filter tests
deno test --filter "addition"
npm Compatibility
Deno can use npm packages:
// Import from npm:
import express from "npm:express@4";
import React from "npm:react@18";
import { PrismaClient } from "npm:@prisma/client";
// Use like normal
const app = express();
app.get('/', (req, res) => {
res.send('Hello from Express in Deno!');
});
app.listen(3000);
Node.js Compatibility Layer
// Import Node.js built-ins
import { readFileSync } from "node:fs";
import { createServer } from "node:http";
import path from "node:path";
// Use Node.js APIs
const data = readFileSync('file.txt', 'utf8');
const server = createServer((req, res) => {
res.end('Hello');
});
Database Support
PostgreSQL
import { Client } from "https://deno.land/x/postgres/mod.ts";
const client = new Client({
user: "user",
database: "test",
hostname: "localhost",
port: 5432,
});
await client.connect();
const result = await client.queryArray("SELECT * FROM users");
console.log(result.rows);
await client.end();
SQLite
import { DB } from "https://deno.land/x/sqlite/mod.ts";
const db = new DB("test.db");
db.query(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT
)
`);
db.query("INSERT INTO users (name, email) VALUES (?, ?)", ["Alice", "[email protected]"]);
const users = db.query("SELECT * FROM users");
for (const [id, name, email] of users) {
console.log({ id, name, email });
}
db.close();
MongoDB
import { MongoClient } from "npm:mongodb@6";
const client = new MongoClient("mongodb://localhost:27017");
await client.connect();
const db = client.db("mydb");
const users = db.collection("users");
await users.insertOne({ name: "Alice", email: "[email protected]" });
const allUsers = await users.find({}).toArray();
console.log(allUsers);
Environment Variables
# .env
DATABASE_URL=postgresql://localhost/mydb
API_KEY=secret123
// Load .env file
import "https://deno.land/[email protected]/dotenv/load.ts";
// Access variables
const dbUrl = Deno.env.get("DATABASE_URL");
const apiKey = Deno.env.get("API_KEY");
// Or set programmatically
Deno.env.set("KEY", "value");
Frameworks
Oak (Express-like)
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
const router = new Router();
router
.get('/', (ctx) => {
ctx.response.body = 'Home';
})
.get('/users/:id', (ctx) => {
ctx.response.body = `User ${ctx.params.id}`;
});
const app = new Application();
app.use(router.routes());
app.use(router.allowedMethods());
await app.listen({ port: 3000 });
Fresh (Full-stack framework)
# Create Fresh app
deno run -A -r https://fresh.deno.dev
cd my-app
deno task start
// routes/index.tsx
import { Head } from "$fresh/runtime.ts";
export default function Home() {
return (
<>
<Head>
<title>Fresh App</title>
</Head>
<div>
<h1>Welcome to Fresh!</h1>
</div>
</>
);
}
WebAssembly
Run WebAssembly in Deno:
const wasmCode = await Deno.readFile("./add.wasm");
const wasmModule = new WebAssembly.Module(wasmCode);
const wasmInstance = new WebAssembly.Instance(wasmModule);
const add = wasmInstance.exports.add as (a: number, b: number) => number;
console.log(add(5, 3)); // 8
Deployment
Deno Deploy
# Install deployctl
deno install --allow-all --no-check -r -f https://deno.land/x/deploy/deployctl.ts
# Deploy
deployctl deploy --project=my-project server.ts
Or use GitHub integration for automatic deployments.
Docker
FROM denoland/deno:1.38.0
WORKDIR /app
# Cache dependencies
COPY deps.ts .
RUN deno cache deps.ts
# Copy application
COPY . .
RUN deno cache server.ts
EXPOSE 8000
CMD ["run", "--allow-net", "--allow-env", "server.ts"]
Traditional Server
# Compile to executable
deno compile --allow-net --output server server.ts
# Upload and run
./server
Deno vs Node.js vs Bun
Feature | Deno | Node.js | Bun |
---|---|---|---|
Security | Permissions required | Full access | Full access |
TypeScript | Native | Via ts-node | Native |
Package Manager | URL imports | npm | Built-in (fast) |
Std Library | Excellent | Good | Good |
Ecosystem | Growing | Huge | Growing |
Performance | Good | Good | Excellent |
Node Compat | Yes (via npm:) | Native | Yes |
Best For | Security, TypeScript | Production | Performance |
Migrating from Node.js
Step 1: Install Deno
curl -fsSL https://deno.land/install.sh | sh
Step 2: Convert Imports
// Node.js
const express = require('express');
const fs = require('fs');
// Deno
import express from "npm:express@4";
import { readFile } from "node:fs/promises";
// Or use Deno APIs
import { readTextFile } from "https://deno.land/std/fs/mod.ts";
Step 3: Update File Operations
// Node.js
const fs = require('fs').promises;
await fs.readFile('file.txt', 'utf8');
// Deno
await Deno.readTextFile('file.txt');
Step 4: Add Permissions
# Node.js
node server.js
# Deno
deno run --allow-net --allow-read server.ts
Best Practices
1. Pin Versions in URLs
// ✓ Good - pinned version
import { serve } from "https://deno.land/[email protected]/http/server.ts";
// ❌ Bad - unpinned (breaks when updated)
import { serve } from "https://deno.land/std/http/server.ts";
2. Use deps.ts for Centralized Imports
// deps.ts
export { serve } from "https://deno.land/[email protected]/http/server.ts";
export { Application } from "https://deno.land/x/[email protected]/mod.ts";
// server.ts
import { serve, Application } from "./deps.ts";
3. Minimize Permissions
# ✓ Good - only what's needed
deno run --allow-net=:8000 --allow-read=./public server.ts
# ❌ Bad - too permissive
deno run -A server.ts
4. Use deno.json for Configuration
{
"tasks": {
"dev": "deno run --watch --allow-net server.ts",
"start": "deno run --allow-net server.ts",
"test": "deno test --allow-net"
}
}
Run: deno task dev
Key Takeaways
- Security-first with explicit permissions
- Native TypeScript with zero configuration
- No package.json or node_modules (URL imports)
- Built-in tooling (formatter, linter, test runner)
- Web standard APIs (fetch, URL, etc.)
- npm compatible for existing packages
- Great for TypeScript projects and security-conscious apps
Related Topics
- Node.js - Original JavaScript runtime
- Bun - Performance-focused alternative
- JavaScript Runtimes Overview - Compare all runtimes
- TypeScript - Works natively in Deno
- Serverless & Edge - Deploy Deno to the edge
Deno represents a modern, secure approach to JavaScript runtimes. While its ecosystem is smaller than Node.js, its focus on security, TypeScript, and web standards makes it an excellent choice for new projects, especially those prioritizing type safety and security.