Deno 2 Complete Guide | Node.js Compatibility, npm, and JSR
이 글의 핵심
Deno 2 is a secure JavaScript runtime that now runs Node.js and npm packages natively. With built-in TypeScript, a permissions model, and the new JSR package registry, it's a compelling alternative to Node.js for new projects.
What Changed in Deno 2
Deno 2 (released October 2024) made Node.js compatibility a first-class feature:
| Feature | Deno 1 | Deno 2 |
|---|---|---|
| npm packages | Partial | Full native support |
node_modules/ | Not created | Created automatically |
package.json | Ignored | Fully supported |
require() | No | Yes |
| Node built-ins | Partial | Full (node:fs, node:path, etc.) |
| JSR | No | Yes |
Installation
# macOS / Linux
curl -fsSL https://deno.land/install.sh | sh
# Windows
irm https://deno.land/install.ps1 | iex
# Homebrew
brew install deno
# Check version
deno --version
1. Running Code
# Run a local file
deno run server.ts
# Run with permissions
deno run --allow-net --allow-read --allow-env server.ts
# Run a URL directly
deno run https://deno.land/std/examples/welcome.ts
# Run npm package
deno run npm:cowsay "Hello Deno"
# Watch mode (re-run on changes)
deno run --watch server.ts
Permission flags
--allow-net # all network access
--allow-net=api.com # only api.com
--allow-read # all filesystem reads
--allow-read=./data # only ./data directory
--allow-write # all filesystem writes
--allow-env # all env vars
--allow-env=API_KEY # only API_KEY
--allow-run # spawn subprocesses
--allow-all # all permissions (like Node.js — use carefully)
-A # shorthand for --allow-all
2. TypeScript — Zero Config
Deno runs TypeScript natively without compilation:
// server.ts — just run it, no tsconfig needed
interface User {
id: number
name: string
email: string
}
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
}
const user = await fetchUser(1)
console.log(`Hello, ${user.name}!`)
deno run --allow-net server.ts
3. npm Compatibility
// Use npm packages with npm: prefix
import express from 'npm:express@4'
import { z } from 'npm:zod'
import axios from 'npm:axios'
const app = express()
app.get('/', (req, res) => {
res.json({ message: 'Express on Deno!' })
})
app.listen(3000)
deno run --allow-net server.ts
# Creates node_modules/ automatically — no npm install needed
Using package.json (Node.js project)
// package.json — Deno 2 respects this
{
"dependencies": {
"express": "^4.18.0",
"zod": "^3.22.0"
}
}
# Install (like npm install but with Deno)
deno install
# Run
deno run --allow-net server.ts
4. JSR — JavaScript Registry
JSR is a TypeScript-first registry that works across runtimes:
// Import from JSR (TypeScript-native packages)
import { Hono } from 'jsr:@hono/hono'
import { join } from 'jsr:@std/path'
import { assertEquals } from 'jsr:@std/assert'
const app = new Hono()
app.get('/', (c) => c.text('Hello!'))
Deno.serve(app.fetch)
// deno.json — lock JSR versions
{
"imports": {
"@std/path": "jsr:@std/path@^1.0.0",
"@std/assert": "jsr:@std/assert@^1.0.0",
"hono": "jsr:@hono/hono@^4.0.0",
"zod": "npm:zod@^3.22.0"
}
}
Publishing to JSR:
# Publish your package to JSR
deno publish
5. Standard Library
Deno’s standard library (@std) covers common needs:
import { join, dirname, basename } from 'jsr:@std/path'
import { exists, readTextFile, writeTextFile } from 'jsr:@std/fs'
import { serve } from 'jsr:@std/http'
import { assertEquals, assertThrows } from 'jsr:@std/assert'
import { delay } from 'jsr:@std/async'
import { parse } from 'jsr:@std/csv'
import { encodeBase64, decodeBase64 } from 'jsr:@std/encoding'
// File operations
const content = await Deno.readTextFile('./config.json')
const config = JSON.parse(content)
await Deno.writeTextFile('./output.txt', 'Hello, Deno!')
// Directory
for await (const entry of Deno.readDir('./src')) {
console.log(entry.name, entry.isFile ? 'file' : 'dir')
}
// Path
const full = join(Deno.cwd(), 'src', 'index.ts')
6. HTTP Server
// Native Deno HTTP (recommended for new projects)
const handler = async (request: Request): Promise<Response> => {
const url = new URL(request.url)
if (url.pathname === '/') {
return new Response('Hello from Deno!', {
headers: { 'Content-Type': 'text/plain' },
})
}
if (url.pathname === '/api/users' && request.method === 'GET') {
const users = await fetchUsers()
return Response.json(users)
}
return new Response('Not Found', { status: 404 })
}
// Deno.serve — the recommended way
Deno.serve({ port: 3000 }, handler)
With Hono (recommended for APIs)
import { Hono } from 'jsr:@hono/hono'
import { cors } from 'jsr:@hono/hono/cors'
import { jwt } from 'jsr:@hono/hono/jwt'
const app = new Hono()
app.use('*', cors())
app.get('/api/health', (c) => c.json({ status: 'ok' }))
app.get('/api/users/:id', async (c) => {
const id = c.req.param('id')
const user = await getUser(id)
if (!user) return c.json({ error: 'Not found' }, 404)
return c.json(user)
})
Deno.serve({ port: 3000 }, app.fetch)
7. Built-in Tooling
Deno ships with everything built in — no npm install needed:
# Format
deno fmt # format all .ts/.js files
deno fmt src/ # format directory
deno fmt --check # check without modifying
# Lint
deno lint # lint all files
deno lint src/
# Test
deno test # run all *_test.ts files
deno test --watch # watch mode
deno test src/user_test.ts # specific file
# Benchmark
deno bench # run all *_bench.ts files
# Type check
deno check src/index.ts
# Bundle
deno compile --allow-net src/cli.ts # compile to single binary
# REPL
deno
Testing
// user_test.ts
import { assertEquals, assertRejects } from 'jsr:@std/assert'
Deno.test('adds two numbers', () => {
assertEquals(1 + 2, 3)
})
Deno.test('fetches user', async () => {
const user = await fetchUser(1)
assertEquals(user.id, 1)
assertEquals(typeof user.name, 'string')
})
Deno.test('throws on invalid id', async () => {
await assertRejects(
() => fetchUser(-1),
Error,
'Invalid user ID'
)
})
// Group tests
Deno.test('UserService', async (t) => {
await t.step('create', async () => {
const user = await createUser({ name: 'Alice' })
assertEquals(user.name, 'Alice')
})
await t.step('delete', async () => {
await deleteUser(user.id)
// ...
})
})
8. deno.json Configuration
{
"tasks": {
"start": "deno run --allow-net --allow-env src/main.ts",
"dev": "deno run --watch --allow-net --allow-env src/main.ts",
"test": "deno test --allow-net",
"lint": "deno lint",
"fmt": "deno fmt"
},
"imports": {
"hono": "jsr:@hono/hono@^4.0.0",
"zod": "npm:zod@^3.22.0",
"@std/assert": "jsr:@std/assert@^1.0.0"
},
"compilerOptions": {
"strict": true,
"lib": ["deno.window"]
},
"fmt": {
"lineWidth": 100,
"indentWidth": 2,
"singleQuote": true
},
"lint": {
"rules": {
"include": ["no-unused-vars", "eqeqeq"]
}
}
}
# Run tasks
deno task start
deno task dev
deno task test
Migrating from Node.js
// Node.js
import { readFileSync } from 'fs'
import { join } from 'path'
import { createServer } from 'http'
// Deno 2 (Node.js compat)
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { createServer } from 'node:http'
// Same code works! Just add "node:" prefix to built-ins
# Quick compatibility check
deno run --allow-all server.js # try running existing Node.js file
Deno vs Node.js vs Bun
| Deno 2 | Node.js 22 | Bun 1.x | |
|---|---|---|---|
| TypeScript | Native | Via tsx/ts-node | Native |
| npm compat | Full | Native | Full |
| Security | Permissions model | Full access | Full access |
| Built-in tools | fmt, lint, test, bench | None | Bundler, test |
| Performance | Fast | Fast | Fastest |
| Ecosystem | npm + JSR | npm | npm |
| Best for | Security-conscious, new projects | Existing ecosystem | Max speed |
Key Takeaways
- Deno 2 runs Node.js and npm packages natively —
npm:express,node:fs - No install step —
deno runfetches dependencies on first run - Permissions model — explicit flags required for network, filesystem, env access
- JSR — TypeScript-first registry, works across Deno/Node/Bun/browsers
- Built-in tooling —
deno fmt,deno lint,deno test,deno compile(no config needed) - deno.json — replaces package.json, tsconfig.json, .eslintrc, .prettierrc