deno
Deno runtime, Deploy, Fresh framework, KV, npm compatibility, and secure-by-default JavaScript/TypeScript
SKILL.md.claude/skills/deno/SKILL.md
GitHubDeno
Purpose
Build and deploy secure-by-default TypeScript/JavaScript applications using Deno 2.x. Covers the Deno runtime, Fresh framework for full-stack web apps, Deno KV for built-in key-value storage, Deno Deploy for edge hosting, and npm compatibility for leveraging the existing ecosystem.
Core Patterns
deno.json Configuration
{
"tasks": {
"dev": "deno run --watch --allow-net --allow-read --allow-env main.ts",
"start": "deno run --allow-net --allow-read --allow-env main.ts",
"test": "deno test --allow-all",
"check": "deno check **/*.ts",
"fmt": "deno fmt",
"lint": "deno lint"
},
"imports": {
"@std/http": "jsr:@std/http@^1.0.0",
"@std/assert": "jsr:@std/assert@^1.0.0",
"@std/path": "jsr:@std/path@^1.0.0",
"hono": "jsr:@hono/hono@^4.0.0",
"zod": "npm:zod@^3.23.0"
},
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
},
"nodeModulesDir": "auto",
"fmt": {
"lineWidth": 100,
"semiColons": true
},
"lint": {
"rules": {
"exclude": ["no-unused-vars"]
}
}
}
HTTP Server (Deno.serve)
// main.ts — Deno's built-in HTTP server
Deno.serve({ port: 8000 }, async (req: Request): Promise<Response> => {
const url = new URL(req.url);
if (url.pathname === "/api/health") {
return Response.json({ status: "ok", timestamp: Date.now() });
}
if (url.pathname === "/api/users" && req.method === "POST") {
const body = await req.json();
// process body...
return Response.json({ id: crypto.randomUUID(), ...body }, { status: 201 });
}
return new Response("Not Found", { status: 404 });
});
Deno KV (Built-in Key-Value Store)
const kv = await Deno.openKv(); // Local SQLite in dev, managed on Deno Deploy
// Set a value with optional expiration
await kv.set(["users", "u123"], { name: "Alice", email: "alice@example.com" });
await kv.set(["sessions", sessionId], { userId: "u123" }, { expireIn: 3600_000 });
// Get a value
const entry = await kv.get<{ name: string }>(["users", "u123"]);
console.log(entry.value?.name); // "Alice"
// List by prefix
const iter = kv.list<{ name: string }>({ prefix: ["users"] });
for await (const entry of iter) {
console.log(entry.key, entry.value);
}
// Atomic transactions (check-and-set)
const current = await kv.get(["counters", "visits"]);
await kv.atomic()
.check(current) // Ensures no concurrent modification
.set(["counters", "visits"], (current.value as number ?? 0) + 1)
.commit();
// Enqueue background jobs
await kv.enqueue({ type: "send-email", to: "alice@example.com", subject: "Welcome" });
kv.listenQueue(async (msg: { type: string; to: string; subject: string }) => {
if (msg.type === "send-email") {
await sendEmail(msg.to, msg.subject);
}
});
Fresh Framework (Full-Stack Web)
my-fresh-app/
routes/
index.tsx # Page routes
about.tsx
api/
users.ts # API routes
_app.tsx # App wrapper
_layout.tsx # Shared layout
_404.tsx # Custom 404
greet/
[name].tsx # Dynamic route
islands/
Counter.tsx # Interactive client components
components/
Header.tsx # Static server components
static/
styles.css
fresh.config.ts
deno.json
// routes/index.tsx — Server-rendered page
import { page, define } from "fresh";
import Counter from "../islands/Counter.tsx";
export const handler = define.handlers({
async GET(ctx) {
const posts = await fetchPosts();
return page({ posts });
},
});
export default define.page<typeof handler>(({ data }) => {
return (
<main class="p-6">
<h1 class="text-2xl font-bold">Blog</h1>
{data.posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
{/* Only this island ships JS to the client */}
<Counter start={0} />
</main>
);
});
// islands/Counter.tsx — Client-side interactive component
import { useSignal } from "@preact/signals";
export default function Counter({ start }: { start: number }) {
const count = useSignal(start);
return (
<div class="flex items-center gap-4">
<button
class="px-6 py-4 text-base rounded-lg bg-blue-600 text-white transition-all duration-200 hover:bg-blue-700 focus-visible:ring-2 focus-visible:ring-offset-2"
onClick={() => count.value--}
>
-
</button>
<span class="text-xl">{count}</span>
<button
class="px-6 py-4 text-base rounded-lg bg-blue-600 text-white transition-all duration-200 hover:bg-blue-700 focus-visible:ring-2 focus-visible:ring-offset-2"
onClick={() => count.value++}
>
+
</button>
</div>
);
}
// routes/api/users.ts — API route
import { define } from "fresh";
export const handler = define.handlers({
async GET(_ctx) {
const kv = await Deno.openKv();
const users = [];
for await (const entry of kv.list({ prefix: ["users"] })) {
users.push(entry.value);
}
return Response.json(users);
},
async POST(ctx) {
const body = await ctx.req.json();
const kv = await Deno.openKv();
const id = crypto.randomUUID();
await kv.set(["users", id], { id, ...body });
return Response.json({ id, ...body }, { status: 201 });
},
});
npm Compatibility
// Import npm packages directly
import express from "npm:express@4";
import { z } from "npm:zod@3";
// Or use import map in deno.json (preferred)
// "imports": { "zod": "npm:zod@^3.23.0" }
import { z } from "zod";
// Node built-in APIs
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import process from "node:process";
const content = await readFile(join(process.cwd(), "data.json"), "utf-8");
Permissions System
# Granular permissions
deno run --allow-net=api.example.com,deno.land main.ts
deno run --allow-read=./data,./config main.ts
deno run --allow-env=DATABASE_URL,API_KEY main.ts
deno run --allow-write=./output main.ts
# Development (allow all — never use in production)
deno run --allow-all main.ts
# Compile to single binary
deno compile --allow-net --allow-read --output myapp main.ts
Best Practices
- Use JSR imports over npm where available —
jsr:@std/*packages are Deno-native and type-safe - Specify granular permissions — never deploy with
--allow-all; whitelist specific hosts, paths, and env vars - Use
deno.jsonimport maps instead of inlinenpm:specifiers for cleaner imports - Leverage Deno KV for session storage, feature flags, rate limiting — zero config on Deno Deploy
- Use Fresh islands sparingly — keep most components server-rendered; only island-ify what needs interactivity
- Pin dependency versions in
deno.jsonand commitdeno.lockfor reproducible builds - Use
Deno.servefor HTTP — it handles graceful shutdown and is optimized for the runtime - Run
deno checkin CI — it type-checks without running, catching errors before deployment - Use
deno fmtanddeno lint— built-in, zero config, no eslint/prettier setup needed
Anti-Patterns
| Anti-Pattern | Why It's Bad | Do Instead |
|---|---|---|
--allow-all in production |
Defeats Deno's security model | Specify exact permissions needed |
Inline npm: everywhere |
Hard to manage versions | Use deno.json import map |
| Making everything an island | Ships unnecessary JS to client | Only island-ify interactive parts |
Ignoring deno.lock |
Non-reproducible builds | Commit lockfile, use --frozen in CI |
Using node:fs when Deno.readTextFile exists |
Misses Deno-native async APIs | Prefer Deno.* APIs |
| KV without atomic checks | Race conditions on concurrent writes | Use kv.atomic().check() for CAS |
Skipping deno check |
Type errors caught only at runtime | Add deno check to CI pipeline |
Decision Guide
| Scenario | Recommendation |
|---|---|
| Full-stack web app with SSR | Fresh framework with Preact islands |
| API server, edge deployment | Deno.serve + Deno Deploy |
| Need robust routing/middleware | Hono on Deno (jsr:@hono/hono) |
| Simple KV/session storage | Deno KV (zero config, built-in) |
| Need relational database | Use npm:postgres or npm:drizzle-orm |
| Existing Node.js project | Deno 2.x with nodeModulesDir: "auto" for gradual migration |
| CLI tool distribution | deno compile for single-binary output |
| Need npm package ecosystem | Import via npm: specifier or import map |