| name | deno |
| description | Deno 2 runtime: built-in TypeScript, permission model, deno.json config, npm compatibility, standard library, Deno KV, Deno Deploy edge runtime, Fresh framework islands architecture, and testing with Deno.test |
Deno 2 Skill
When to activate
- Starting a new TypeScript project and evaluating runtimes
- Building a security-sensitive service where granular permissions matter
- Deploying to Deno Deploy (edge, global distribution)
- Using the Fresh framework (Deno's web framework)
- Working with Deno KV (built-in key-value store, no external dependency)
- Migrating a small Node.js script to Deno
- The project uses
deno.json or import_map.json instead of package.json
- User mentions
deno run, deno task, deno deploy, or @std/ imports
When NOT to use
- Projects already built on Node.js with deep native addon dependencies — migration cost is high
- Bun projects — different runtime, use the
bun skill
- Cloudflare Workers — use the
hono skill (Workers has its own runtime)
- Projects requiring npm packages with native bindings not yet ported to Deno
- When the team has no Deno familiarity and a deadline is tight — Node.js ecosystem is larger
Instructions
Project setup
curl -fsSL https://deno.land/install.sh | sh
deno --version
mkdir my-api && cd my-api
{
"tasks": {
"dev": "deno run --watch --allow-net --allow-env --allow-read src/main.ts",
"start": "deno run --allow-net --allow-env --allow-read src/main.ts",
"test": "deno test --allow-net --allow-env",
"check": "deno check src/main.ts"
},
"imports": {
"@std/http": "jsr:@std/http@^1.0.0",
"@std/path": "jsr:@std/path@^1.0.0",
"@std/testing": "jsr:@std/testing@^1.0.0",
"hono": "jsr:@hono/hono@^4.0.0",
"zod": "npm:zod@^3.22.0"
},
"compilerOptions": {
No node_modules. No tsconfig.json. No babel.config.js. TypeScript runs directly.
Permission model
Deno is deny-by-default. Every capability must be explicitly granted at startup.
deno run --allow-net src/main.ts
deno run --allow-net=api.example.com src/main.ts
deno run --allow-read src/main.ts
deno run --allow-read=/tmp src/main.ts
deno run --allow-write=/tmp src/main.ts
deno run --allow-env src/main.ts
deno run --allow-env=PORT,DATABASE_URL src/main.ts
deno run --allow-run=git src/main.ts
deno run -A src/main.ts
You can also declare permissions in the script itself (Deno 2):
const { granted } = await Deno.permissions.request({ name: 'env', variable: 'DATABASE_URL' })
if (!granted) throw new Error('DATABASE_URL env permission denied')
npm compatibility
Deno 2 supports npm packages directly — no installation step.
import { z } from 'npm:zod'
import express from 'npm:express@4'
import { PrismaClient } from 'npm:@prisma/client'
import { z } from 'zod'
import { Hono } from 'jsr:@hono/hono'
import { assertEquals } from 'jsr:@std/assert'
deno cache src/main.ts
deno outdated
Standard library (@std/)
import { serve } from '@std/http'
await serve((req) => {
const url = new URL(req.url)
if (url.pathname === '/health') {
return Response.json({ status: 'ok' })
}
return new Response('Not found', { status: 404 })
}, { port: 8000 })
import { join, dirname, basename, extname } from '@std/path'
const configPath = join(Deno.cwd(), 'config', 'settings.json')
const text = await Deno.readTextFile(configPath)
const config = JSON.parse(text)
await Deno.writeTextFile('/tmp/output.json', JSON.stringify(config, , ))
port = (..() ?? )
dbUrl = ..()
(!dbUrl) ()
Deno KV
Built-in key-value store. Zero config. Works locally and on Deno Deploy.
const kv = await Deno.openKv()
await kv.set(['users', 'alice@example.com'], {
id: 'u_01',
name: 'Alice',
email: 'alice@example.com',
createdAt: new Date().toISOString(),
})
const result = await kv.get<{ name: string }>(['users', 'alice@example.com'])
console.log(result.value?.name)
console.log(result.versionstamp)
const iter = kv.list<User>({ prefix: ['users'] })
const users: User[] = []
for await (const entry iter) {
users.(entry.)
}
res = kv.()
.({ : [, token], : })
.([, token], { : , : .() + })
.()
(!res.) ()
kv.([, token])
watcher = kv.([[, ]])
( [entry] watcher) {
.(, entry.)
}
Testing
import { assertEquals, assertRejects, assertExists } from '@std/assert'
Deno.test('adds two numbers', () => {
assertEquals(1 + 2, 3)
})
Deno.test('fetches user from KV', async () => {
const kv = await Deno.openKv(':memory:')
await kv.set(['users', '1'], { name: 'Alice' })
const result = await kv.get(['users', '1'])
assertExists(result.value)
assertEquals((result.value as { name: string }).name, 'Alice')
kv.close()
})
import { describe, it, beforeAll, afterAll } from '@std/testing/bdd'
{ assertThrows }
(, {
: .
( () => { kv = .() })
( kv.())
(, () => {
(
(kv, ),
,
,
)
})
})
.({
: ,
: { : [] },
() {
res = ()
(res., )
},
})
deno test
deno test --watch
deno test --filter "UserService"
deno test --coverage=./cov
deno coverage ./cov
deno test --allow-net src/user.test.ts
Fresh framework (islands architecture)
deno run -A -r jsr:@fresh/init my-app
cd my-app && deno task start
my-app/
├── routes/
│ ├── index.tsx # server-rendered page at /
│ ├── blog/
│ │ └── [slug].tsx # dynamic route
│ └── api/
│ └── users.ts # API handler (no JSX)
├── islands/
│ └── Counter.tsx # client-side interactive component
├── components/
│ └── Button.tsx # server-only component (no JS sent)
├── deno.json
└── main.ts # entry point
import type { PageProps } from '$fresh/server.ts'
import Counter from '../islands/Counter.tsx'
export default function Home({ data }: PageProps) {
return (
<main>
<h1>Hello from Fresh</h1>
{/* Counter is an "island" — only this component ships JS to the browser */}
<Counter initialCount={0} />
</main>
)
}
import { useSignal } from '@preact/signals'
export default function Counter({ initialCount }: { initialCount: number }) {
const count = useSignal(initialCount)
return (
<button onClick={() => count.value++}>
Count: {count}
</button>
)
}
import { Handlers } from '$fresh/server.ts'
export const handler: Handlers = {
async GET(req, ctx) {
const users = await getUsersFromKv()
return Response.json({ users })
},
async POST(req, ctx) {
const body = await req.json()
const user = await createUserInKv(body)
return Response.json(user, { status: 201 })
},
}
Deno Deploy
deno install -A jsr:@deno/deployctl
deployctl deploy --project=my-api src/main.ts
const kv = await Deno.openKv()
Deno.serve({ port: 8000 }, async (req) => {
const url = new URL(req.url)
if (url.pathname === '/') {
const visits = await kv.get<number>(['visits'])
const count = (visits.value ?? 0) + 1
await kv.set(['visits'], count)
return Response.json({ visits: count })
}
return new Response('Not found', { status: 404 })
})
Deno vs Node.js — when to choose Deno
Choose Deno when:
- Greenfield TypeScript project — no tsconfig, no build step, TypeScript works out of the box
- Security matters — explicit permissions make the attack surface auditable;
--allow-net=payments.stripe.com is self-documenting
- Edge deployment — Deno Deploy is purpose-built for it; ~300 PoPs, cold-start in microseconds
- Simple scripts —
deno run https://example.com/script.ts works without installing anything
- Deno KV — need a persistent store without setting up Redis or Postgres for a small service
Stick with Node.js when:
- Existing large codebase with native addons
- Team is deep in Node.js ecosystem tooling (Jest, Webpack, etc.)
- Packages used have no Deno/npm-compatible builds
- Monorepo tooling (Turborepo, Nx) that assumes Node.js
Example
User: Build a REST API with Hono on Deno that stores items in Deno KV. Needs CRUD endpoints and a test.
Expected output:
{
"tasks": {
"dev": "deno run --watch --allow-net --allow-env src/main.ts",
"test": "deno test --allow-env"
},
"imports": {
"hono": "jsr:@hono/hono@^4.0.0",
"zod": "npm:zod@^3.22.0",
"@std/assert": "jsr:@std/assert@^1.0.0"
}
}
import { Hono } from 'hono'
import { z } from 'zod'
const kv = await Deno.openKv()
const app = new Hono()
const ItemSchema = z.object({
name: z.string().min(1),
quantity: z.number().int().positive(),
})
app.get('/items', async (c) => {
const items: unknown[] = []
const iter = kv.list({ prefix: ['items'] })
for await (const entry of iter) {
items.push(entry.value)
}
return c.json({ items })
})
app.get('/items/:id', async (c) => {
const result = await kv.get([, c..()])
(!result.) c.({ : }, )
c.(result.)
})
app.(, (c) => {
body = c..()
parsed = .(body)
(!parsed.) {
c.({ : parsed..(). }, )
}
id = crypto.()
item = { id, ...parsed. }
kv.([, id], item)
c.(item, )
})
app.(, (c) => {
kv.([, c..()])
(, { : })
})
.({ : }, app.)
import { assertEquals } from '@std/assert'
import app from './main.ts'
Deno.test('POST /items creates and GET retrieves', async () => {
const createRes = await app.fetch(
new Request('http://localhost/items', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: 'Apples', quantity: 10 }),
})
)
assertEquals(createRes.status, 201)
const item = await createRes.json()
assertEquals(item.name, 'Apples')
const getRes = await app.fetch(new Request(`http://localhost/items/${item.id}`))
assertEquals(getRes.status, 200)
const fetched = await getRes.json()
(fetched., )
})
Deploy: deployctl deploy --project=my-inventory src/main.ts — globally distributed in seconds, KV replicated automatically.