Quick Start: Up and Running in 5 Minutes
If you’ve been hearing about Deno but kept putting it off, Deno 2.0 is the version that finally makes it click. I picked it up while looking for a way to run TypeScript scripts without a build step. What surprised me: the whole install-and-run cycle took under three minutes, and I never touched a tsconfig.
Install Deno with a single command — no npm, no node_modules:
# macOS / Linux
curl -fsSL https://deno.land/install.sh | sh
# Windows (PowerShell)
irm https://deno.land/install.ps1 | iex
# Or via package managers
brew install deno # macOS Homebrew
winget install DenoLand.Deno # Windows
Verify the install:
deno --version
# deno 2.0.x (release, ...)
Now run your first TypeScript file — no compiler, no config file:
// hello.ts
const name: string = "Deno 2.0";
console.log(`Hello from ${name}!`);
deno run hello.ts
# Hello from Deno 2.0!
That’s it. TypeScript runs directly. Compare that to a typical Node + TypeScript setup: npm init, install ts-node or tsx, write a tsconfig.json, then finally run your file. Deno skips all of it.
Deep Dive: Building a CLI Tool and HTTP Server
Building a CLI Tool
Writing CLI scripts is where Deno really shines. Your colleagues don’t need Node, npm, or any runtime installed — you compile to a single binary and ship it. That alone saves a lot of “works on my machine” headaches.
Here’s a CLI tool that reads a file and counts word frequency:
// wordcount.ts
const args = Deno.args;
if (args.length === 0) {
console.error("Usage: deno run --allow-read wordcount.ts <filename>");
Deno.exit(1);
}
const filename = args[0];
const text = await Deno.readTextFile(filename);
const words = text.toLowerCase().match(/\b\w+\b/g) ?? [];
const freq: Record<string, number> = {};
for (const word of words) {
freq[word] = (freq[word] ?? 0) + 1;
}
const sorted = Object.entries(freq)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
console.log("Top 10 words:");
for (const [word, count] of sorted) {
console.log(` ${word}: ${count}`);
}
deno run --allow-read wordcount.ts README.md
The --allow-read flag is intentional. Deno’s security model is explicit: scripts can’t touch files, network, or environment variables unless you grant it. That might feel clunky on day one. But when you’re running a third-party script in production and want to confirm it can’t phone home or modify your filesystem, you’ll appreciate having a concrete list of what it’s allowed to do.
Compile to a Single Binary
Once your CLI tool works, compile it into a standalone executable. The output is typically 60–80 MB for a simple tool — that’s the Deno runtime bundled in.
# Compile for current platform
deno compile --allow-read wordcount.ts -o wordcount
# Cross-compile for other platforms
deno compile --target x86_64-pc-windows-msvc --allow-read wordcount.ts -o wordcount.exe
deno compile --target x86_64-apple-darwin --allow-read wordcount.ts -o wordcount-mac
deno compile --target x86_64-unknown-linux-gnu --allow-read wordcount.ts -o wordcount-linux
Drop that binary on any machine — no runtime needed.
Building an HTTP Server
Deno 2.0 ships with a production-ready HTTP server. For many use cases, you don’t need Express or Fastify at all:
// server.ts
const PORT = 8000;
const handler = (req: Request): Response => {
const url = new URL(req.url);
if (url.pathname === "/") {
return new Response("Welcome to my Deno server!", {
headers: { "Content-Type": "text/plain" },
});
}
if (url.pathname === "/health") {
return Response.json({ status: "ok", timestamp: new Date().toISOString() });
}
if (url.pathname === "/echo" && req.method === "POST") {
const body = req.body;
return new Response(body, {
headers: { "Content-Type": req.headers.get("Content-Type") ?? "text/plain" },
});
}
return new Response("Not Found", { status: 404 });
};
console.log(`Server running at http://localhost:${PORT}`);
Deno.serve({ port: PORT }, handler);
deno run --allow-net server.ts
# Test it
curl http://localhost:8000/health
# {"status":"ok","timestamp":"2026-05-21T10:00:00.000Z"}
The handler uses standard Request and Response objects — the exact same Web APIs your browser exposes. If you’ve written any Fetch API code before, this will feel familiar immediately.
Advanced Usage: Integrating npm Packages
Using npm Packages Directly
Deno 2.0 treats npm as a first-class citizen. You can import packages using the npm: specifier, no node_modules directory involved:
// Using npm packages with npm: specifier
import express from "npm:express@4";
import chalk from "npm:chalk@5";
const app = express();
app.get("/", (req, res) => {
console.log(chalk.green("Request received!"));
res.json({ message: "Hello from Deno + Express!" });
});
app.listen(3000, () => {
console.log(chalk.blue("Server running on port 3000"));
});
deno run --allow-net --allow-read --allow-env app.ts
Setting Up a deno.json Config File
Real projects benefit from a deno.json — it centralizes your import aliases and task scripts, similar to package.json:
{
"imports": {
"@std/fs": "jsr:@std/fs@1",
"@std/path": "jsr:@std/path@1",
"zod": "npm:zod@3",
"chalk": "npm:chalk@5"
},
"tasks": {
"start": "deno run --allow-net --allow-read server.ts",
"dev": "deno run --watch --allow-net --allow-read server.ts",
"build": "deno compile --allow-net --allow-read server.ts -o myserver",
"test": "deno test --allow-read"
},
"compilerOptions": {
"strict": true
}
}
Run tasks the same way you’d use npm scripts:
deno task dev # development with hot reload
deno task build # compile to binary
deno task test # run tests
A Practical Example: Validated API with Zod
Here’s a more complete server using Zod for input validation. This is closer to what you’d actually deploy:
// api.ts
import { z } from "zod";
const UserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().min(18),
});
const handler = async (req: Request): Promise<Response> => {
if (req.method === "POST" && new URL(req.url).pathname === "/users") {
try {
const body = await req.json();
const user = UserSchema.parse(body);
// In a real app: save to database here
return Response.json({ success: true, user }, { status: 201 });
} catch (err) {
if (err instanceof z.ZodError) {
return Response.json(
{ success: false, errors: err.errors },
{ status: 400 }
);
}
return Response.json({ success: false, message: "Invalid JSON" }, { status: 400 });
}
}
return new Response("Not Found", { status: 404 });
};
Deno.serve({ port: 8000 }, handler);
# Valid request
curl -X POST http://localhost:8000/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "[email protected]", "age": 25}'
# Invalid request — Zod catches it
curl -X POST http://localhost:8000/users \
-H "Content-Type: application/json" \
-d '{"name": "", "email": "not-an-email", "age": 15}'
Practical Tips
Use –watch for Development
The --watch flag restarts your script on file changes. No nodemon, no extra install:
deno run --watch --allow-net server.ts
Understand the Permission Model Early
During early development, use --allow-all so you’re not guessing flags. Then lock things down before deploying:
# Development: open permissions
deno run --allow-all server.ts
# Production: only what's needed
deno run --allow-net=:8000 --allow-read=./data server.ts
Testing Is Built In
Deno ships with its own test runner. No Jest, no Mocha, no config file needed:
// wordcount_test.ts
import { assertEquals } from "jsr:@std/assert";
Deno.test("counts words correctly", () => {
const text = "hello world hello";
const words = text.match(/\b\w+\b/g) ?? [];
const freq: Record<string, number> = {};
for (const word of words) freq[word] = (freq[word] ?? 0) + 1;
assertEquals(freq["hello"], 2);
assertEquals(freq["world"], 1);
});
deno test wordcount_test.ts
Cache Dependencies for Offline Use
Deno downloads dependencies on first run and caches them locally. For CI pipelines or restricted environments, pre-cache everything upfront:
# Install and cache all dependencies from deno.json
deno install
# Or cache a specific entry file with a lockfile
deno cache --lock=deno.lock server.ts
Deploying to Production
Your compiled binary runs on any Linux server — Deno itself is not required on the target machine:
# Build
deno compile --target x86_64-unknown-linux-gnu \
--allow-net --allow-read \
server.ts -o myserver
# Deploy
scp myserver user@your-server:/opt/myapp/
ssh user@your-server '/opt/myapp/myserver'
Alternatively, push to GitHub and let Deno Deploy handle the rest — no server config, no Docker, just a live URL.
Coming from Node.js, expect roughly a week to feel fully comfortable. The explicit permissions feel like friction at first. After a few days they become second nature, and you start appreciating that every script you review has a clear declaration of what it can access. Start small — build one internal CLI tool. That single project will teach you 80% of what you need to know about how Deno fits together.

