Deno 2 Complete Guide | Node.js Compatibility, npm, and JSR

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:

FeatureDeno 1Deno 2
npm packagesPartialFull native support
node_modules/Not createdCreated automatically
package.jsonIgnoredFully supported
require()NoYes
Node built-insPartialFull (node:fs, node:path, etc.)
JSRNoYes

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)
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 2Node.js 22Bun 1.x
TypeScriptNativeVia tsx/ts-nodeNative
npm compatFullNativeFull
SecurityPermissions modelFull accessFull access
Built-in toolsfmt, lint, test, benchNoneBundler, test
PerformanceFastFastFastest
Ecosystemnpm + JSRnpmnpm
Best forSecurity-conscious, new projectsExisting ecosystemMax speed

Key Takeaways

  • Deno 2 runs Node.js and npm packages natively — npm:express, node:fs
  • No install stepdeno run fetches 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 toolingdeno fmt, deno lint, deno test, deno compile (no config needed)
  • deno.json — replaces package.json, tsconfig.json, .eslintrc, .prettierrc