Deno: The Complete Guide to the Secure JavaScript and TypeScript Runtime
Deno is a modern, secure JavaScript and TypeScript runtime built on V8 and Rust, created by Ryan Dahl — the same developer who built Node.js. After years of reflection on the design decisions he would have made differently, Dahl introduced Deno in 2018 with a clear philosophy: security by default, first-class TypeScript support, web-standard APIs, and a single executable with no external dependencies. With the release of Deno 2.x, the runtime has matured into a production-ready platform that offers full npm and Node.js compatibility while preserving everything that makes it distinct.
Why Deno Matters for Web Developers
The JavaScript ecosystem has long been defined by complexity. A typical Node.js project requires a package manager, a separate TypeScript compiler, a bundler, a linter, a formatter, and a test runner — each with its own configuration file and upgrade cycle. Deno collapses all of this into a single binary. The built-in formatter, linter, test runner, type checker, and documentation generator are part of the runtime itself, not optional additions. This reduces project setup from hours to minutes and eliminates an entire class of dependency conflicts.
Security is the most fundamental design difference between Deno and Node.js. In Node.js, any script you run has unrestricted access to the file system, network, environment variables, and system APIs the moment you execute it. Deno inverts this default: a Deno program runs in a sandbox and must explicitly request access to each capability it needs. If your HTTP server should not write to disk, it cannot — unless you grant that permission.
Deno 2.x achieves full npm and Node.js compatibility, resolving the biggest practical objection to adoption. You can import packages from npm directly using the npm: specifier, use the node: built-in prefix for Node.js core modules, and run many existing Node.js applications with minimal changes. The jsr: registry hosts packages published with TypeScript source, explicit provenance, and quality scoring.
TypeScript is a first-class citizen in Deno, not an afterthought. You can write .ts files and run them directly with deno run file.ts — no tsc, no ts-node, no build step. Type checking is integrated, and the language server works out of the box in VS Code.
Step 1: System Requirements
- macOS 10.15 (Catalina) or later
- Ubuntu 18.04+ or any modern Linux distribution
- Windows 10/11 or Windows Server 2019+
- 64-bit processor (x86-64 or ARM64)
- 512 MB RAM minimum; 2 GB+ recommended
Step 2: Install Deno
macOS and Linux
curl -fsSL https://deno.land/install.sh | sh
Add to your shell config:
export DENO_INSTALL="$HOME/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"
macOS (Homebrew)
brew install deno
Windows (PowerShell)
irm https://deno.land/install.ps1 | iex
Verify:
deno --version
Upgrade:
deno upgrade
Step 3: Project Setup
mkdir my-deno-app && cd my-deno-app
deno init
The deno.json Configuration File
{
"tasks": {
"dev": "deno run --watch --allow-net --allow-env main.ts",
"start": "deno run --allow-net --allow-env main.ts",
"test": "deno test --allow-net --allow-env",
"lint": "deno lint",
"fmt": "deno fmt"
},
"imports": {
"@std/http": "jsr:@std/http@^1.0.0",
"@std/path": "jsr:@std/path@^1.0.0",
"oak": "jsr:@oak/oak@^17.0.0"
},
"compilerOptions": {
"strict": true
},
"fmt": {
"lineWidth": 100,
"indentWidth": 2
}
}
Dependency Management
deno cache main.ts
deno install --frozen
Deno stores modules in a global cache — no node_modules folder. Commit deno.lock to version control.
Step 4: Configure Deno for Your Workflow
The Permissions Model
--allow-net/--allow-net=api.example.com— network access--allow-read/--allow-read=/app/data— file system read--allow-write=/app/logs— file system write--allow-env/--allow-env=PORT,DATABASE_URL— environment variables--allow-run— subprocesses-A/--allow-all— all permissions (development only)
Defining Tasks
{
"tasks": {
"dev": "deno run --watch --allow-net --allow-env --allow-read=. main.ts",
"start": "deno run --allow-net --allow-env main.ts",
"test": "deno test --allow-net",
"build": "deno compile --allow-net --allow-env --output=./dist/server main.ts"
}
}
deno task dev
Step 5: Core Features
Deno.serve
const PORT = parseInt(Deno.env.get("PORT") ?? "8000");
Deno.serve({ port: PORT }, async (request: Request) => {
const url = new URL(request.url);
if (url.pathname === "/health") {
return new Response(JSON.stringify({ status: "ok" }), {
headers: { "Content-Type": "application/json" },
});
}
if (url.pathname === "/api/greet" && request.method === "POST") {
const body = await request.json();
return Response.json({ message: `Hello, ${body.name ?? "World"}!` });
}
return new Response("Not Found", { status: 404 });
});
Standard Library
@std/http— HTTP utilities, cookies, SSE@std/path— Cross-platform path manipulation@std/fs— File system utilities@std/assert— Testing assertions@std/encoding— Base64, hex, CSV, YAML
npm Compatibility
import { Hono } from "npm:hono@4";
import { z } from "npm:zod@3";
const app = new Hono();
app.post("/orders", async (c) => {
const body = await c.req.json();
return c.json({ orderId: crypto.randomUUID() }, 201);
});
Deno.serve(app.fetch);
Building with Deno? DeployHQ connects your Git repo to any server and deploys automatically when you push — SFTP, SSH, or cloud. Try it free.
Step 6: Advanced Features
Fresh Framework
deno run -A -r https://fresh.deno.dev my-fresh-app
Fresh uses island-based architecture — pages render on the server by default, interactive components are selectively hydrated.
Oak Middleware Framework
import { Application, Router } from "jsr:@oak/oak@17";
const router = new Router();
router
.get("/api/posts", (ctx) => { ctx.response.body = { posts: [] }; })
.post("/api/posts", async (ctx) => {
const body = await ctx.request.body.json();
ctx.response.status = 201;
ctx.response.body = { created: body };
});
const app = new Application();
app.use(router.routes());
app.use(router.allowedMethods());
await app.listen({ port: 8000 });
Deno KV
const kv = await Deno.openKv();
await kv.set(["users", "alice"], {
name: "Alice",
email: "alice@example.com",
});
const result = await kv.get(["users", "alice"]);
const iter = kv.list({ prefix: ["users"] });
for await (const entry of iter) {
console.log(entry.key, entry.value);
}
Compile to Binary
deno compile \
--allow-net \
--allow-env \
--target x86_64-unknown-linux-gnu \
--output dist/server \
main.ts
Cross-compile targets: x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-apple-darwin, aarch64-apple-darwin, x86_64-pc-windows-msvc.
Step 7: Best Practices
Apply the Principle of Least Permission
deno run \
--allow-read=/app/config,/app/static \
--allow-write=/app/logs \
--allow-net=api.stripe.com,db.internal \
--allow-env=DATABASE_URL,STRIPE_KEY \
main.ts
Pin Dependency Versions
Commit deno.lock. Use --frozen in CI/production.
Write Tests with the Built-in Runner
import { assertEquals } from "@std/assert";
Deno.test("createUser returns correct name", async () => {
const user = await createUser({ name: "Alice", email: "alice@example.com" });
assertEquals(user.name, "Alice");
});
deno test --allow-net
deno test --coverage=./coverage
Step 8: Deploy with DeployHQ
Strategy A: Deploy a Compiled Binary (Recommended)
Build command:
deno compile \
--allow-net \
--allow-env \
--target x86_64-unknown-linux-gnu \
--output dist/server \
main.ts
Systemd service:
[Unit]
Description=My Deno Application
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/my-deno-app
ExecStart=/var/www/my-deno-app/dist/server
Restart=always
RestartSec=5
EnvironmentFile=/etc/my-deno-app/env
[Install]
WantedBy=multi-user.target
Post-deployment command:
sudo systemctl restart my-deno-app
Strategy B: Deploy Source and Run with Deno
Install Deno on the server:
curl -fsSL https://deno.land/install.sh | sh
Systemd service:
[Unit]
Description=My Deno Application
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/my-deno-app
ExecStart=/root/.deno/bin/deno run \
--allow-net=0.0.0.0 \
--allow-env=PORT,DATABASE_URL,JWT_SECRET \
--frozen \
main.ts
Restart=always
EnvironmentFile=/etc/my-deno-app/env
[Install]
WantedBy=multi-user.target
Environment Variables
Store secrets in /etc/my-deno-app/env:
PORT=8000
DATABASE_URL=postgresql://user:pass@localhost:5432/myapp
JWT_SECRET=your-secret-key-here
Reverse Proxy
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Step 9: Troubleshooting
Permission Denied Errors
Deno tells you exactly which permission is missing. Add the required flag or recompile the binary.
Module Not Found
deno cache --reload main.ts
Lock File Conflicts
deno cache main.ts
git add deno.lock && git commit -m "Update lock file"
Application Crashes on Startup After Deployment
sudo journalctl -u my-deno-app -n 100 --no-pager
Common causes: missing environment variables, incorrect --allow-read/--allow-write paths, or wrong target architecture for compiled binary.
High Memory Usage
deno run --v8-flags=--max-old-space-size=256 --allow-net main.ts
Conclusion
Deno represents a thoughtful evolution in the JavaScript runtime space. By learning from a decade of Node.js usage patterns, it delivers a runtime that is secure by default, ships with all the tooling you need, and embraces TypeScript as a first-class language. The deno.json file eliminates toolchain fragmentation. The permissions model makes production deployments safer. And deno compile simplifies distribution to a degree previously reserved for compiled languages.
Deploying Deno with DeployHQ is straightforward whether you choose the compiled binary approach or the source-with-runtime approach. Connect your repository, configure your build commands, add your post-deployment restart hook, and DeployHQ handles the rest on every push.
If you have questions about deploying your Deno projects, reach out to us at support@deployhq.com or find us on Twitter/X.