Deno - Bezpieczny Runtime JavaScript i TypeScript
Czym jest Deno?
Deno to nowoczesny runtime JavaScript i TypeScript stworzony przez Ryana Dahla - tego samego programistę, który stworzył Node.js. Po latach doświadczeń z Node.js, Ryan dostrzegł fundamentalne problemy w architekturze Node (brak bezpieczeństwa, package.json, node_modules) i postanowił stworzyć runtime od nowa, naprawiając te błędy.
Deno oferuje natywne wsparcie TypeScript bez konfiguracji, granularne uprawnienia bezpieczeństwa (domyślnie program nie ma dostępu do niczego), wbudowane narzędzia deweloperskie (formatter, linter, test runner) oraz zgodność z Web API. Od wersji 2.0 Deno jest w pełni kompatybilny z npm i Node.js, co pozwala na płynną migrację istniejących projektów.
Nazwa "Deno" to anagram słowa "Node" - symbolizuje to ewolucję i przeprojektowanie koncepcji runtime JavaScript.
Dlaczego Deno?
Kluczowe zalety Deno
- Bezpieczeństwo by Default - Program domyślnie nie ma dostępu do sieci, plików ani zmiennych środowiskowych
- Natywny TypeScript - Uruchamia .ts bez kompilacji i konfiguracji
- Wbudowane narzędzia - Formatter, linter, test runner, bundler, REPL
- Web API - Zgodność z przeglądarkowymi API (fetch, URL, Streams)
- ESM Only - Tylko nowoczesne moduły ES, bez CommonJS
- Single Executable - Kompilacja do jednego pliku wykonywalnego
- npm Compatibility - Pełna kompatybilność z pakietami npm
- Deno Deploy - Serverless edge deployment w 35+ regionach
Deno vs Node.js vs Bun
| Cecha | Deno | Node.js | Bun |
|---|---|---|---|
| TypeScript | Natywnie | Wymaga ts-node/tsx | Natywnie |
| Permissions | Granularne | Brak (pełny dostęp) | Brak |
| ES Modules | Jedyne wspierane | Opcjonalne | Domyślne |
| CommonJS | Częściowe (compat) | Natywne | Natywne |
| package.json | Opcjonalny | Wymagany | Wymagany |
| node_modules | Opcjonalne | Wymagane | Wymagane |
| Web API | Pełna zgodność | Częściowa | Częściowa |
| Bundler | Wbudowany | Wymaga webpack/vite | Wbudowany |
| Test Runner | Wbudowany | Wymaga Jest/Vitest | Wbudowany |
| npm Support | ✅ (od 2.0) | ✅ | ✅ |
| Szybkość | Szybki | Średni | Najszybszy |
| Edge Deploy | Deno Deploy | Vercel/Cloudflare | - |
Kiedy wybrać Deno?
Deno jest idealny gdy:
- Zależy Ci na bezpieczeństwie (sandboxing, audyt)
- Chcesz natywny TypeScript bez konfiguracji
- Budujesz edge functions (Deno Deploy)
- Preferujesz wbudowane narzędzia
- Chcesz Web-compatible API
- Tworzysz CLI tools (single executable)
Rozważ alternatywy gdy:
- Masz duży istniejący projekt Node.js
- Potrzebujesz specyficznych pakietów npm bez ESM
- Maksymalna szybkość jest priorytetem (Bun)
- Twój zespół zna tylko Node.js
Instalacja i konfiguracja
Instalacja Deno
# macOS / Linux (curl)
curl -fsSL https://deno.land/install.sh | sh
# macOS (Homebrew)
brew install deno
# Windows (PowerShell)
irm https://deno.land/install.ps1 | iex
# Windows (Chocolatey)
choco install deno
# Windows (Scoop)
scoop install deno
# Weryfikacja instalacji
deno --version
# deno 2.x.x
# v8 12.x.x
# typescript 5.x.xAktualizacja Deno
# Aktualizacja do najnowszej wersji
deno upgrade
# Aktualizacja do konkretnej wersji
deno upgrade --version 2.0.0Konfiguracja VS Code
# Zainstaluj oficjalne rozszerzenie
code --install-extension denoland.vscode-deno// .vscode/settings.json
{
"deno.enable": true,
"deno.lint": true,
"deno.unstable": false,
"editor.defaultFormatter": "denoland.vscode-deno",
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"[typescriptreact]": {
"editor.defaultFormatter": "denoland.vscode-deno"
}
}deno.json - Konfiguracja projektu
{
"name": "@myorg/my-project",
"version": "1.0.0",
"exports": "./mod.ts",
"tasks": {
"dev": "deno run --watch --allow-net --allow-read main.ts",
"test": "deno test --allow-read",
"lint": "deno lint",
"fmt": "deno fmt",
"check": "deno check main.ts",
"compile": "deno compile --allow-net --allow-read -o app main.ts"
},
"imports": {
"@std/": "jsr:@std/",
"oak": "jsr:@oak/oak@^17.0.0",
"zod": "npm:zod@^3.23.0",
"~/": "./src/"
},
"compilerOptions": {
"strict": true,
"lib": ["deno.window", "dom"]
},
"lint": {
"include": ["src/"],
"exclude": ["src/generated/"],
"rules": {
"tags": ["recommended"],
"include": ["no-unused-vars"]
}
},
"fmt": {
"useTabs": false,
"lineWidth": 100,
"indentWidth": 2,
"semiColons": false,
"singleQuote": true
},
"test": {
"include": ["tests/"]
}
}System uprawnień (Permissions)
Domyślnie program Deno nie ma dostępu do niczego - musi jawnie poprosić o każde uprawnienie.
Dostępne uprawnienia
# Dostęp do sieci
--allow-net # Wszystkie hosty
--allow-net=api.example.com # Tylko konkretny host
--allow-net=:8080 # Tylko port 8080
# Dostęp do plików
--allow-read # Odczyt wszystkich plików
--allow-read=/data,./config # Tylko konkretne ścieżki
--allow-write # Zapis wszystkich plików
--allow-write=/tmp # Tylko /tmp
# Zmienne środowiskowe
--allow-env # Wszystkie zmienne
--allow-env=API_KEY,DB_URL # Tylko konkretne zmienne
# Uruchamianie procesów
--allow-run # Wszystkie procesy
--allow-run=git,npm # Tylko git i npm
# FFI (Foreign Function Interface)
--allow-ffi # Ładowanie bibliotek natywnych
# System
--allow-sys # Informacje o systemie
# High-resolution time
--allow-hrtime # Dokładne pomiary czasu
# Wszystkie uprawnienia (development only!)
--allow-all # Lub -APrzykłady użycia uprawnień
# Serwer HTTP z odczytem plików
deno run --allow-net --allow-read server.ts
# CLI tool z dostępem do git
deno run --allow-run=git --allow-read=. deploy.ts
# API client z konkretnym hostem
deno run --allow-net=api.github.com github-client.ts
# Skrypt z dostępem do .env
deno run --allow-read=.env --allow-env config.tsProgramowe sprawdzanie uprawnień
// Sprawdź uprawnienie przed użyciem
const netStatus = await Deno.permissions.query({
name: "net",
host: "api.example.com"
})
if (netStatus.state === "granted") {
// Mamy dostęp
await fetch("https://api.example.com/data")
} else if (netStatus.state === "prompt") {
// Możemy poprosić
const result = await Deno.permissions.request({
name: "net",
host: "api.example.com"
})
if (result.state === "granted") {
await fetch("https://api.example.com/data")
}
}Natywny TypeScript
Zero konfiguracji
// main.ts - po prostu uruchom: deno run main.ts
interface User {
id: number
name: string
email: string
createdAt: Date
}
interface ApiResponse<T> {
data: T
status: number
message: string
}
async function fetchUser(id: number): Promise<ApiResponse<User>> {
const response = await fetch(`https://api.example.com/users/${id}`)
const data = await response.json()
return {
data: data as User,
status: response.status,
message: response.ok ? "Success" : "Error"
}
}
const result = await fetchUser(1)
console.log(result.data.name)# Uruchom bez żadnej konfiguracji
deno run --allow-net main.tsType checking
# Sprawdź typy bez uruchamiania
deno check main.ts
# Sprawdź typy w trybie watch
deno check --watch main.ts
# Sprawdź wszystkie pliki
deno check **/*.tsImport i zarządzanie zależnościami
Import z różnych źródeł
// Standard library (JSR)
import { join } from "jsr:@std/path"
import { serve } from "jsr:@std/http"
import { assertEquals } from "jsr:@std/assert"
// npm packages
import { z } from "npm:zod@^3.23.0"
import express from "npm:express@^4.18.0"
import _ from "npm:lodash"
// Direct URL (legacy, ale nadal wspierane)
import { Application } from "https://deno.land/x/oak@v17.0.0/mod.ts"
// Lokalne pliki
import { config } from "./config.ts"
import { utils } from "~/utils/mod.ts" // Z import map
// Node.js built-in modules
import process from "node:process"
import fs from "node:fs"
import path from "node:path"Import Maps w deno.json
{
"imports": {
// Standard library
"@std/": "jsr:@std/",
// npm packages
"zod": "npm:zod@^3.23.0",
"hono": "npm:hono@^4.0.0",
"drizzle-orm": "npm:drizzle-orm@^0.30.0",
// Aliasy lokalne
"~/": "./src/",
"@components/": "./src/components/",
"@utils/": "./src/utils/",
// Konkretne wersje
"lodash": "npm:lodash@4.17.21"
}
}// Teraz możesz importować krócej
import { z } from "zod"
import { Hono } from "hono"
import { UserService } from "~/services/user.ts"
import { Button } from "@components/Button.tsx"JSR - JavaScript Registry
JSR to nowoczesny registry dla pakietów JavaScript/TypeScript, stworzony przez zespół Deno.
# Publikowanie pakietu na JSR
deno publish
# Dodawanie pakietu z JSR
deno add @std/http
deno add @oak/oak// Import z JSR
import { serve } from "jsr:@std/http@1.0.0"
import { assertEquals } from "jsr:@std/assert@1.0.0"Web API i Deno API
Fetch API (zgodne z przeglądarką)
// GET request
const response = await fetch("https://api.example.com/users")
const users = await response.json()
// POST request z JSON
const createResponse = await fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
body: JSON.stringify({
name: "Jan Kowalski",
email: "jan@example.com"
})
})
// Streaming response
const streamResponse = await fetch("https://api.example.com/stream")
const reader = streamResponse.body?.getReader()
while (true) {
const { done, value } = await reader!.read()
if (done) break
console.log(new TextDecoder().decode(value))
}URL API
const url = new URL("https://example.com/path?foo=bar")
url.searchParams.set("page", "2")
url.searchParams.append("filter", "active")
console.log(url.href) // https://example.com/path?foo=bar&page=2&filter=active
console.log(url.pathname) // /path
console.log(url.hostname) // example.comWeb Streams
// ReadableStream
const readable = new ReadableStream({
start(controller) {
controller.enqueue("Hello ")
controller.enqueue("World!")
controller.close()
}
})
// TransformStream
const uppercase = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase())
}
})
// Pipe streams
const result = readable.pipeThrough(uppercase)
for await (const chunk of result) {
console.log(chunk) // "HELLO " "WORLD!"
}Deno-specific API
// Odczyt plików
const content = await Deno.readTextFile("./config.json")
const bytes = await Deno.readFile("./image.png")
// Zapis plików
await Deno.writeTextFile("./output.txt", "Hello World")
await Deno.writeFile("./data.bin", new Uint8Array([1, 2, 3]))
// Informacje o pliku
const stat = await Deno.stat("./file.txt")
console.log(stat.isFile, stat.size, stat.mtime)
// Lista plików
for await (const entry of Deno.readDir("./src")) {
console.log(entry.name, entry.isDirectory)
}
// Zmienne środowiskowe
const apiKey = Deno.env.get("API_KEY")
Deno.env.set("DEBUG", "true")
// Argumenty CLI
const args = Deno.args // ["--port", "3000"]
// Uruchamianie procesów
const command = new Deno.Command("git", {
args: ["status"],
stdout: "piped",
stderr: "piped"
})
const { code, stdout, stderr } = await command.output()
console.log(new TextDecoder().decode(stdout))HTTP Server
Deno.serve (wbudowany)
// Najprostszy serwer
Deno.serve((req: Request) => {
return new Response("Hello Deno!")
})
// Z konfiguracją
Deno.serve({
port: 3000,
hostname: "0.0.0.0",
onListen: ({ port, hostname }) => {
console.log(`Server running at http://${hostname}:${port}`)
}
}, handler)
// Handler z routingiem
function handler(req: Request): Response {
const url = new URL(req.url)
switch (url.pathname) {
case "/":
return new Response("Home")
case "/api/users":
if (req.method === "GET") {
return Response.json([{ id: 1, name: "Jan" }])
}
if (req.method === "POST") {
return Response.json({ created: true }, { status: 201 })
}
return new Response("Method not allowed", { status: 405 })
default:
return new Response("Not found", { status: 404 })
}
}Streaming Response
Deno.serve((req: Request) => {
const body = new ReadableStream({
async start(controller) {
for (let i = 0; i < 5; i++) {
controller.enqueue(new TextEncoder().encode(`Event ${i}\n`))
await new Promise(r => setTimeout(r, 1000))
}
controller.close()
}
})
return new Response(body, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache"
}
})
})Server-Sent Events
Deno.serve((req: Request) => {
const url = new URL(req.url)
if (url.pathname === "/events") {
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder()
const interval = setInterval(() => {
const event = `data: ${JSON.stringify({ time: new Date() })}\n\n`
controller.enqueue(encoder.encode(event))
}, 1000)
// Cleanup on close
req.signal.addEventListener("abort", () => {
clearInterval(interval)
controller.close()
})
}
})
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive"
}
})
}
return new Response("Not found", { status: 404 })
})Frameworki Web
Hono (rekomendowany)
// main.ts
import { Hono } from "npm:hono"
import { cors } from "npm:hono/cors"
import { logger } from "npm:hono/logger"
import { z } from "npm:zod"
import { zValidator } from "npm:@hono/zod-validator"
const app = new Hono()
// Middleware
app.use("*", logger())
app.use("/api/*", cors())
// Routes
app.get("/", (c) => c.text("Hello Hono!"))
app.get("/api/users", async (c) => {
const users = await db.query.users.findMany()
return c.json(users)
})
// Walidacja z Zod
const createUserSchema = z.object({
name: z.string().min(2),
email: z.string().email()
})
app.post(
"/api/users",
zValidator("json", createUserSchema),
async (c) => {
const body = c.req.valid("json")
const user = await db.insert(users).values(body).returning()
return c.json(user, 201)
}
)
// Groups
const api = new Hono()
api.get("/posts", (c) => c.json([]))
api.get("/posts/:id", (c) => c.json({ id: c.req.param("id") }))
app.route("/api", api)
// Error handling
app.onError((err, c) => {
console.error(err)
return c.json({ error: "Internal Server Error" }, 500)
})
// Start
Deno.serve(app.fetch)Oak
import { Application, Router } from "jsr:@oak/oak"
const router = new Router()
router.get("/", (ctx) => {
ctx.response.body = "Hello Oak!"
})
router.get("/api/users", async (ctx) => {
const users = await fetchUsers()
ctx.response.body = users
})
router.post("/api/users", async (ctx) => {
const body = await ctx.request.body.json()
const user = await createUser(body)
ctx.response.status = 201
ctx.response.body = user
})
const app = new Application()
// Logger middleware
app.use(async (ctx, next) => {
const start = Date.now()
await next()
const ms = Date.now() - start
console.log(`${ctx.request.method} ${ctx.request.url} - ${ms}ms`)
})
app.use(router.routes())
app.use(router.allowedMethods())
console.log("Server running on http://localhost:8000")
await app.listen({ port: 8000 })Fresh (Full-stack framework)
Fresh to oficjalny full-stack framework od Deno z island architecture i zero JS by default.
# Tworzenie projektu Fresh
deno run -A -r https://fresh.deno.dev my-project
cd my-project
deno task start// routes/index.tsx
import { Handlers, PageProps } from "$fresh/server.ts"
interface Data {
posts: Array<{ id: number; title: string }>
}
export const handler: Handlers<Data> = {
async GET(req, ctx) {
const posts = await fetchPosts()
return ctx.render({ posts })
}
}
export default function Home({ data }: PageProps<Data>) {
return (
<div>
<h1>Blog</h1>
<ul>
{data.posts.map((post) => (
<li key={post.id}>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
))}
</ul>
</div>
)
}
// islands/Counter.tsx - Interactive component (hydrated)
import { useState } from "preact/hooks"
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
)
}Testowanie
Wbudowany test runner
// math.ts
export function add(a: number, b: number): number {
return a + b
}
export function divide(a: number, b: number): number {
if (b === 0) throw new Error("Division by zero")
return a / b
}
// math_test.ts
import { assertEquals, assertThrows } from "jsr:@std/assert"
import { add, divide } from "./math.ts"
Deno.test("add function", () => {
assertEquals(add(2, 3), 5)
assertEquals(add(-1, 1), 0)
assertEquals(add(0, 0), 0)
})
Deno.test("divide function", () => {
assertEquals(divide(10, 2), 5)
assertEquals(divide(7, 2), 3.5)
})
Deno.test("divide by zero throws", () => {
assertThrows(
() => divide(1, 0),
Error,
"Division by zero"
)
})
// Async test
Deno.test("async fetch", async () => {
const response = await fetch("https://api.example.com/data")
assertEquals(response.status, 200)
})
// Test z cleanup
Deno.test({
name: "database test",
async fn() {
const db = await connectDb()
try {
const result = await db.query("SELECT 1")
assertEquals(result.rows.length, 1)
} finally {
await db.close()
}
},
sanitizeResources: false,
sanitizeOps: false
})# Uruchom wszystkie testy
deno test
# Testy z uprawnieniami
deno test --allow-net --allow-read
# Konkretny plik
deno test math_test.ts
# Watch mode
deno test --watch
# Coverage
deno test --coverage=coverage
deno coverage coverageMocking
import { stub, assertSpyCalls } from "jsr:@std/testing/mock"
Deno.test("mocking fetch", async () => {
const fetchStub = stub(
globalThis,
"fetch",
() => Promise.resolve(new Response(JSON.stringify({ id: 1 })))
)
try {
const user = await fetchUser(1)
assertEquals(user.id, 1)
assertSpyCalls(fetchStub, 1)
} finally {
fetchStub.restore()
}
})Wbudowane narzędzia
Formatter
# Formatuj wszystkie pliki
deno fmt
# Sprawdź bez modyfikacji
deno fmt --check
# Konkretne pliki
deno fmt src/main.ts
# Ignoruj foldery
deno fmt --ignore=vendor,node_modulesLinter
# Lint wszystkich plików
deno lint
# Konkretne pliki
deno lint src/
# Pokaż reguły
deno lint --rulesBundler
# Bundle do jednego pliku
deno bundle main.ts bundle.js
# Bundle z minifikacją (użyj esbuild)
deno run -A npm:esbuild main.ts --bundle --minify --outfile=out.jsKompilacja do wykonywalnego pliku
# Kompiluj z embedded permissions
deno compile --allow-net --allow-read main.ts
# Z nazwą i target
deno compile --output app --target x86_64-unknown-linux-gnu main.ts
# Dostępne targets:
# x86_64-unknown-linux-gnu
# x86_64-pc-windows-msvc
# x86_64-apple-darwin
# aarch64-apple-darwinDeno Deploy
Deno Deploy to serverless edge platform od twórców Deno.
Deploy z CLI
# Zaloguj się
deno install -gArf jsr:@deno/deployctl
# Deploy
deployctl deploy --project=my-project main.ts
# Z entrypoint
deployctl deploy --project=my-project --entrypoint=server.tsDeploy z GitHub Actions
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Install Deno
uses: denoland/setup-deno@v1
- name: Deploy to Deno Deploy
uses: denoland/deployctl@v1
with:
project: my-project
entrypoint: main.tsKonfiguracja projektu dla Deploy
// main.ts - kompatybilny z Deno Deploy
Deno.serve((req: Request) => {
const url = new URL(req.url)
if (url.pathname === "/api/hello") {
return Response.json({ message: "Hello from the edge!" })
}
return new Response("Not found", { status: 404 })
})Deno KV (Key-Value Store)
// Otwórz KV store (lokalnie lub na Deno Deploy)
const kv = await Deno.openKv()
// Set
await kv.set(["users", "user-123"], {
name: "Jan",
email: "jan@example.com"
})
// Get
const result = await kv.get<User>(["users", "user-123"])
console.log(result.value) // { name: "Jan", ... }
// Delete
await kv.delete(["users", "user-123"])
// List z prefixem
const entries = kv.list<User>({ prefix: ["users"] })
for await (const entry of entries) {
console.log(entry.key, entry.value)
}
// Atomic operations
const res = await kv.atomic()
.check({ key: ["users", "user-123"], versionstamp: result.versionstamp })
.set(["users", "user-123"], { ...result.value, name: "Jan Updated" })
.commit()
if (!res.ok) {
console.log("Conflict - someone else modified the data")
}Kompatybilność z Node.js
Import Node.js modules
// Node.js built-in modules
import process from "node:process"
import fs from "node:fs/promises"
import path from "node:path"
import { Buffer } from "node:buffer"
// Użycie
console.log(process.env.HOME)
const content = await fs.readFile("./file.txt", "utf-8")
const fullPath = path.join(process.cwd(), "data")npm packages
// Import z npm:
import express from "npm:express@4"
import _ from "npm:lodash"
import { PrismaClient } from "npm:@prisma/client"
// W deno.json (preferowane)
{
"imports": {
"express": "npm:express@^4.18.0",
"lodash": "npm:lodash@^4.17.0"
}
}
// Potem import normalnie
import express from "express"Migracja z Node.js
# Uruchom Node.js projekt w Deno
DENO_FUTURE=1 deno run --allow-all npm:your-script
# Lub użyj compatibility mode
deno run --node-modules-dir --allow-all main.tsBest Practices
Struktura projektu
my-deno-project/
├── deno.json # Konfiguracja
├── deno.lock # Lockfile
├── main.ts # Entry point
├── deps.ts # (opcjonalne) Centralne zależności
├── src/
│ ├── mod.ts # Public API
│ ├── routes/
│ │ ├── users.ts
│ │ └── posts.ts
│ ├── services/
│ │ └── database.ts
│ └── utils/
│ └── helpers.ts
├── tests/
│ ├── routes_test.ts
│ └── services_test.ts
└── scripts/
└── seed.tsCentralizacja zależności (opcjonalne)
// deps.ts - jeden plik ze wszystkimi zewnętrznymi zależnościami
export { z } from "npm:zod@^3.23.0"
export { Hono } from "npm:hono@^4.0.0"
export { assertEquals, assertThrows } from "jsr:@std/assert"
export { join, dirname } from "jsr:@std/path"
// Używanie w innych plikach
import { z, Hono } from "./deps.ts"Type-safe environment variables
// env.ts
import { z } from "npm:zod"
const envSchema = z.object({
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(["development", "production", "test"]).default("development")
})
export const env = envSchema.parse(Deno.env.toObject())FAQ - Najczęściej zadawane pytania
Czy Deno może zastąpić Node.js?
Dla nowych projektów - tak. Deno 2.0 jest w pełni kompatybilny z npm i Node.js API, więc migracja jest możliwa. Dla istniejących dużych projektów Node.js migracja może wymagać wysiłku.
Dlaczego domyślnie brak dostępu do niczego?
To fundamentalna zasada bezpieczeństwa. Skrypt pobrany z internetu nie powinien mieć automatycznie dostępu do Twoich plików czy sieci. Musisz jawnie przyznać uprawnienia.
Czy Deno jest szybszy od Node.js?
W większości przypadków Deno i Node.js mają podobną wydajność. Bun jest szybszy od obu. Deno wyróżnia się natywnym TypeScript i wbudowanymi narzędziami.
Czy mogę używać pakietów npm w Deno?
Tak! Od Deno 2.0 jest pełna kompatybilność z npm. Używaj npm:package-name lub skonfiguruj import maps w deno.json.
Co to jest JSR?
JSR (JavaScript Registry) to nowy registry pakietów od zespołu Deno. Wspiera TypeScript natywnie i oferuje lepsze DX niż npm. Pakiety publikujesz przez deno publish.
Podsumowanie
Deno to nowoczesny runtime JavaScript/TypeScript, który naprawia fundamentalne problemy Node.js:
- Bezpieczeństwo - Granularne uprawnienia, sandboxing by default
- TypeScript - Natywne wsparcie bez konfiguracji
- Web Standards - Zgodność z przeglądarkowymi API
- Wbudowane narzędzia - Formatter, linter, test runner, bundler
- npm Compatibility - Pełna kompatybilność od wersji 2.0
- Deno Deploy - Edge deployment w 35+ regionach
Deno jest idealny dla nowych projektów, CLI tools, edge functions i wszędzie tam, gdzie zależy Ci na bezpieczeństwie i nowoczesnym DX.
Deno - secure JavaScript and TypeScript runtime
What is Deno?
Deno is a modern JavaScript and TypeScript runtime created by Ryan Dahl - the same developer who created Node.js. After years of experience with Node.js, Ryan identified fundamental problems in Node's architecture (lack of security, package.json, node_modules) and decided to build a runtime from scratch, fixing those mistakes.
Deno offers native TypeScript support without configuration, granular security permissions (by default a program has no access to anything), built-in developer tools (formatter, linter, test runner), and Web API compatibility. Since version 2.0 Deno is fully compatible with npm and Node.js, which allows for a smooth migration of existing projects.
The name "Deno" is an anagram of the word "Node" - symbolizing the evolution and redesign of the JavaScript runtime concept.
Why Deno?
Key advantages of Deno
- Security by default - A program has no access to the network, files, or environment variables by default
- Native TypeScript - Runs .ts files without compilation or configuration
- Built-in tools - Formatter, linter, test runner, bundler, REPL
- Web API - Compatibility with browser APIs (fetch, URL, Streams)
- ESM only - Only modern ES modules, no CommonJS
- Single executable - Compile to a single executable file
- npm compatibility - Full compatibility with npm packages
- Deno Deploy - Serverless edge deployment in 35+ regions
Deno vs Node.js vs Bun
| Feature | Deno | Node.js | Bun |
|---|---|---|---|
| TypeScript | Native | Requires ts-node/tsx | Native |
| Permissions | Granular | None (full access) | None |
| ES Modules | Only supported | Optional | Default |
| CommonJS | Partial (compat) | Native | Native |
| package.json | Optional | Required | Required |
| node_modules | Optional | Required | Required |
| Web API | Full compatibility | Partial | Partial |
| Bundler | Built-in | Requires webpack/vite | Built-in |
| Test Runner | Built-in | Requires Jest/Vitest | Built-in |
| npm Support | ✅ (since 2.0) | ✅ | ✅ |
| Speed | Fast | Medium | Fastest |
| Edge Deploy | Deno Deploy | Vercel/Cloudflare | - |
When to choose Deno?
Deno is ideal when:
- Security matters to you (sandboxing, auditing)
- You want native TypeScript without configuration
- You are building edge functions (Deno Deploy)
- You prefer built-in tools
- You want Web-compatible APIs
- You are creating CLI tools (single executable)
Consider alternatives when:
- You have a large existing Node.js project
- You need specific npm packages without ESM support
- Maximum speed is a priority (Bun)
- Your team only knows Node.js
Installation and configuration
Installing Deno
# macOS / Linux (curl)
curl -fsSL https://deno.land/install.sh | sh
# macOS (Homebrew)
brew install deno
# Windows (PowerShell)
irm https://deno.land/install.ps1 | iex
# Windows (Chocolatey)
choco install deno
# Windows (Scoop)
scoop install deno
# Verify installation
deno --version
# deno 2.x.x
# v8 12.x.x
# typescript 5.x.xUpdating Deno
# Update to the latest version
deno upgrade
# Update to a specific version
deno upgrade --version 2.0.0VS Code configuration
# Install the official extension
code --install-extension denoland.vscode-deno// .vscode/settings.json
{
"deno.enable": true,
"deno.lint": true,
"deno.unstable": false,
"editor.defaultFormatter": "denoland.vscode-deno",
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"[typescriptreact]": {
"editor.defaultFormatter": "denoland.vscode-deno"
}
}deno.json - project configuration
{
"name": "@myorg/my-project",
"version": "1.0.0",
"exports": "./mod.ts",
"tasks": {
"dev": "deno run --watch --allow-net --allow-read main.ts",
"test": "deno test --allow-read",
"lint": "deno lint",
"fmt": "deno fmt",
"check": "deno check main.ts",
"compile": "deno compile --allow-net --allow-read -o app main.ts"
},
"imports": {
"@std/": "jsr:@std/",
"oak": "jsr:@oak/oak@^17.0.0",
"zod": "npm:zod@^3.23.0",
"~/": "./src/"
},
"compilerOptions": {
"strict": true,
"lib": ["deno.window", "dom"]
},
"lint": {
"include": ["src/"],
"exclude": ["src/generated/"],
"rules": {
"tags": ["recommended"],
"include": ["no-unused-vars"]
}
},
"fmt": {
"useTabs": false,
"lineWidth": 100,
"indentWidth": 2,
"semiColons": false,
"singleQuote": true
},
"test": {
"include": ["tests/"]
}
}Permissions system
By default, a Deno program has no access to anything - it must explicitly request every permission.
Available permissions
# Network access
--allow-net # All hosts
--allow-net=api.example.com # Only a specific host
--allow-net=:8080 # Only port 8080
# File access
--allow-read # Read all files
--allow-read=/data,./config # Only specific paths
--allow-write # Write all files
--allow-write=/tmp # Only /tmp
# Environment variables
--allow-env # All variables
--allow-env=API_KEY,DB_URL # Only specific variables
# Running processes
--allow-run # All processes
--allow-run=git,npm # Only git and npm
# FFI (Foreign Function Interface)
--allow-ffi # Load native libraries
# System
--allow-sys # System information
# High-resolution time
--allow-hrtime # Precise time measurements
# All permissions (development only!)
--allow-all # Or -APermission usage examples
# HTTP server with file reading
deno run --allow-net --allow-read server.ts
# CLI tool with git access
deno run --allow-run=git --allow-read=. deploy.ts
# API client with a specific host
deno run --allow-net=api.github.com github-client.ts
# Script with .env access
deno run --allow-read=.env --allow-env config.tsProgrammatic permission checking
const netStatus = await Deno.permissions.query({
name: "net",
host: "api.example.com"
})
if (netStatus.state === "granted") {
await fetch("https://api.example.com/data")
} else if (netStatus.state === "prompt") {
const result = await Deno.permissions.request({
name: "net",
host: "api.example.com"
})
if (result.state === "granted") {
await fetch("https://api.example.com/data")
}
}Native TypeScript
Zero configuration
// main.ts - just run: deno run main.ts
interface User {
id: number
name: string
email: string
createdAt: Date
}
interface ApiResponse<T> {
data: T
status: number
message: string
}
async function fetchUser(id: number): Promise<ApiResponse<User>> {
const response = await fetch(`https://api.example.com/users/${id}`)
const data = await response.json()
return {
data: data as User,
status: response.status,
message: response.ok ? "Success" : "Error"
}
}
const result = await fetchUser(1)
console.log(result.data.name)# Run without any configuration
deno run --allow-net main.tsType checking
# Check types without running
deno check main.ts
# Check types in watch mode
deno check --watch main.ts
# Check all files
deno check **/*.tsImports and dependency management
Importing from various sources
// Standard library (JSR)
import { join } from "jsr:@std/path"
import { serve } from "jsr:@std/http"
import { assertEquals } from "jsr:@std/assert"
// npm packages
import { z } from "npm:zod@^3.23.0"
import express from "npm:express@^4.18.0"
import _ from "npm:lodash"
// Direct URL (legacy, but still supported)
import { Application } from "https://deno.land/x/oak@v17.0.0/mod.ts"
// Local files
import { config } from "./config.ts"
import { utils } from "~/utils/mod.ts" // With import map
// Node.js built-in modules
import process from "node:process"
import fs from "node:fs"
import path from "node:path"Import maps in deno.json
{
"imports": {
// Standard library
"@std/": "jsr:@std/",
// npm packages
"zod": "npm:zod@^3.23.0",
"hono": "npm:hono@^4.0.0",
"drizzle-orm": "npm:drizzle-orm@^0.30.0",
// Local aliases
"~/": "./src/",
"@components/": "./src/components/",
"@utils/": "./src/utils/",
// Specific versions
"lodash": "npm:lodash@4.17.21"
}
}// Now you can use shorter imports
import { z } from "zod"
import { Hono } from "hono"
import { UserService } from "~/services/user.ts"
import { Button } from "@components/Button.tsx"JSR - JavaScript Registry
JSR is a modern registry for JavaScript/TypeScript packages, created by the Deno team.
# Publishing a package to JSR
deno publish
# Adding a package from JSR
deno add @std/http
deno add @oak/oak// Import from JSR
import { serve } from "jsr:@std/http@1.0.0"
import { assertEquals } from "jsr:@std/assert@1.0.0"Web API and Deno API
Fetch API (browser-compatible)
// GET request
const response = await fetch("https://api.example.com/users")
const users = await response.json()
// POST request with JSON
const createResponse = await fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
body: JSON.stringify({
name: "Jan Kowalski",
email: "jan@example.com"
})
})
// Streaming response
const streamResponse = await fetch("https://api.example.com/stream")
const reader = streamResponse.body?.getReader()
while (true) {
const { done, value } = await reader!.read()
if (done) break
console.log(new TextDecoder().decode(value))
}URL API
const url = new URL("https://example.com/path?foo=bar")
url.searchParams.set("page", "2")
url.searchParams.append("filter", "active")
console.log(url.href) // https://example.com/path?foo=bar&page=2&filter=active
console.log(url.pathname) // /path
console.log(url.hostname) // example.comWeb Streams
// ReadableStream
const readable = new ReadableStream({
start(controller) {
controller.enqueue("Hello ")
controller.enqueue("World!")
controller.close()
}
})
// TransformStream
const uppercase = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase())
}
})
// Pipe streams
const result = readable.pipeThrough(uppercase)
for await (const chunk of result) {
console.log(chunk) // "HELLO " "WORLD!"
}Deno-specific API
// Reading files
const content = await Deno.readTextFile("./config.json")
const bytes = await Deno.readFile("./image.png")
// Writing files
await Deno.writeTextFile("./output.txt", "Hello World")
await Deno.writeFile("./data.bin", new Uint8Array([1, 2, 3]))
// File information
const stat = await Deno.stat("./file.txt")
console.log(stat.isFile, stat.size, stat.mtime)
// Listing files
for await (const entry of Deno.readDir("./src")) {
console.log(entry.name, entry.isDirectory)
}
// Environment variables
const apiKey = Deno.env.get("API_KEY")
Deno.env.set("DEBUG", "true")
// CLI arguments
const args = Deno.args // ["--port", "3000"]
// Running processes
const command = new Deno.Command("git", {
args: ["status"],
stdout: "piped",
stderr: "piped"
})
const { code, stdout, stderr } = await command.output()
console.log(new TextDecoder().decode(stdout))HTTP server
Deno.serve (built-in)
// Simplest server
Deno.serve((req: Request) => {
return new Response("Hello Deno!")
})
// With configuration
Deno.serve({
port: 3000,
hostname: "0.0.0.0",
onListen: ({ port, hostname }) => {
console.log(`Server running at http://${hostname}:${port}`)
}
}, handler)
// Handler with routing
function handler(req: Request): Response {
const url = new URL(req.url)
switch (url.pathname) {
case "/":
return new Response("Home")
case "/api/users":
if (req.method === "GET") {
return Response.json([{ id: 1, name: "Jan" }])
}
if (req.method === "POST") {
return Response.json({ created: true }, { status: 201 })
}
return new Response("Method not allowed", { status: 405 })
default:
return new Response("Not found", { status: 404 })
}
}Streaming response
Deno.serve((req: Request) => {
const body = new ReadableStream({
async start(controller) {
for (let i = 0; i < 5; i++) {
controller.enqueue(new TextEncoder().encode(`Event ${i}\n`))
await new Promise(r => setTimeout(r, 1000))
}
controller.close()
}
})
return new Response(body, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache"
}
})
})Server-Sent Events
Deno.serve((req: Request) => {
const url = new URL(req.url)
if (url.pathname === "/events") {
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder()
const interval = setInterval(() => {
const event = `data: ${JSON.stringify({ time: new Date() })}\n\n`
controller.enqueue(encoder.encode(event))
}, 1000)
// Cleanup on close
req.signal.addEventListener("abort", () => {
clearInterval(interval)
controller.close()
})
}
})
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive"
}
})
}
return new Response("Not found", { status: 404 })
})Web frameworks
Hono (recommended)
// main.ts
import { Hono } from "npm:hono"
import { cors } from "npm:hono/cors"
import { logger } from "npm:hono/logger"
import { z } from "npm:zod"
import { zValidator } from "npm:@hono/zod-validator"
const app = new Hono()
// Middleware
app.use("*", logger())
app.use("/api/*", cors())
// Routes
app.get("/", (c) => c.text("Hello Hono!"))
app.get("/api/users", async (c) => {
const users = await db.query.users.findMany()
return c.json(users)
})
// Validation with Zod
const createUserSchema = z.object({
name: z.string().min(2),
email: z.string().email()
})
app.post(
"/api/users",
zValidator("json", createUserSchema),
async (c) => {
const body = c.req.valid("json")
const user = await db.insert(users).values(body).returning()
return c.json(user, 201)
}
)
// Groups
const api = new Hono()
api.get("/posts", (c) => c.json([]))
api.get("/posts/:id", (c) => c.json({ id: c.req.param("id") }))
app.route("/api", api)
// Error handling
app.onError((err, c) => {
console.error(err)
return c.json({ error: "Internal Server Error" }, 500)
})
// Start
Deno.serve(app.fetch)Oak
import { Application, Router } from "jsr:@oak/oak"
const router = new Router()
router.get("/", (ctx) => {
ctx.response.body = "Hello Oak!"
})
router.get("/api/users", async (ctx) => {
const users = await fetchUsers()
ctx.response.body = users
})
router.post("/api/users", async (ctx) => {
const body = await ctx.request.body.json()
const user = await createUser(body)
ctx.response.status = 201
ctx.response.body = user
})
const app = new Application()
// Logger middleware
app.use(async (ctx, next) => {
const start = Date.now()
await next()
const ms = Date.now() - start
console.log(`${ctx.request.method} ${ctx.request.url} - ${ms}ms`)
})
app.use(router.routes())
app.use(router.allowedMethods())
console.log("Server running on http://localhost:8000")
await app.listen({ port: 8000 })Fresh (full-stack framework)
Fresh is the official full-stack framework from Deno with island architecture and zero JS by default.
# Creating a Fresh project
deno run -A -r https://fresh.deno.dev my-project
cd my-project
deno task start// routes/index.tsx
import { Handlers, PageProps } from "$fresh/server.ts"
interface Data {
posts: Array<{ id: number; title: string }>
}
export const handler: Handlers<Data> = {
async GET(req, ctx) {
const posts = await fetchPosts()
return ctx.render({ posts })
}
}
export default function Home({ data }: PageProps<Data>) {
return (
<div>
<h1>Blog</h1>
<ul>
{data.posts.map((post) => (
<li key={post.id}>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
))}
</ul>
</div>
)
}
// islands/Counter.tsx - Interactive component (hydrated)
import { useState } from "preact/hooks"
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
)
}Testing
Built-in test runner
// math.ts
export function add(a: number, b: number): number {
return a + b
}
export function divide(a: number, b: number): number {
if (b === 0) throw new Error("Division by zero")
return a / b
}
// math_test.ts
import { assertEquals, assertThrows } from "jsr:@std/assert"
import { add, divide } from "./math.ts"
Deno.test("add function", () => {
assertEquals(add(2, 3), 5)
assertEquals(add(-1, 1), 0)
assertEquals(add(0, 0), 0)
})
Deno.test("divide function", () => {
assertEquals(divide(10, 2), 5)
assertEquals(divide(7, 2), 3.5)
})
Deno.test("divide by zero throws", () => {
assertThrows(
() => divide(1, 0),
Error,
"Division by zero"
)
})
// Async test
Deno.test("async fetch", async () => {
const response = await fetch("https://api.example.com/data")
assertEquals(response.status, 200)
})
// Test with cleanup
Deno.test({
name: "database test",
async fn() {
const db = await connectDb()
try {
const result = await db.query("SELECT 1")
assertEquals(result.rows.length, 1)
} finally {
await db.close()
}
},
sanitizeResources: false,
sanitizeOps: false
})# Run all tests
deno test
# Tests with permissions
deno test --allow-net --allow-read
# Specific file
deno test math_test.ts
# Watch mode
deno test --watch
# Coverage
deno test --coverage=coverage
deno coverage coverageMocking
import { stub, assertSpyCalls } from "jsr:@std/testing/mock"
Deno.test("mocking fetch", async () => {
const fetchStub = stub(
globalThis,
"fetch",
() => Promise.resolve(new Response(JSON.stringify({ id: 1 })))
)
try {
const user = await fetchUser(1)
assertEquals(user.id, 1)
assertSpyCalls(fetchStub, 1)
} finally {
fetchStub.restore()
}
})Built-in tools
Formatter
# Format all files
deno fmt
# Check without modifying
deno fmt --check
# Specific files
deno fmt src/main.ts
# Ignore folders
deno fmt --ignore=vendor,node_modulesLinter
# Lint all files
deno lint
# Specific files
deno lint src/
# Show rules
deno lint --rulesBundler
# Bundle to a single file
deno bundle main.ts bundle.js
# Bundle with minification (use esbuild)
deno run -A npm:esbuild main.ts --bundle --minify --outfile=out.jsCompiling to an executable
# Compile with embedded permissions
deno compile --allow-net --allow-read main.ts
# With a name and target
deno compile --output app --target x86_64-unknown-linux-gnu main.ts
# Available targets:
# x86_64-unknown-linux-gnu
# x86_64-pc-windows-msvc
# x86_64-apple-darwin
# aarch64-apple-darwinDeno Deploy
Deno Deploy is a serverless edge platform from the creators of Deno.
Deploy from CLI
# Log in
deno install -gArf jsr:@deno/deployctl
# Deploy
deployctl deploy --project=my-project main.ts
# With entrypoint
deployctl deploy --project=my-project --entrypoint=server.tsDeploy with GitHub Actions
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Install Deno
uses: denoland/setup-deno@v1
- name: Deploy to Deno Deploy
uses: denoland/deployctl@v1
with:
project: my-project
entrypoint: main.tsProject configuration for Deploy
// main.ts - compatible with Deno Deploy
Deno.serve((req: Request) => {
const url = new URL(req.url)
if (url.pathname === "/api/hello") {
return Response.json({ message: "Hello from the edge!" })
}
return new Response("Not found", { status: 404 })
})Deno KV (Key-Value Store)
// Open KV store (locally or on Deno Deploy)
const kv = await Deno.openKv()
// Set
await kv.set(["users", "user-123"], {
name: "Jan",
email: "jan@example.com"
})
// Get
const result = await kv.get<User>(["users", "user-123"])
console.log(result.value) // { name: "Jan", ... }
// Delete
await kv.delete(["users", "user-123"])
// List with prefix
const entries = kv.list<User>({ prefix: ["users"] })
for await (const entry of entries) {
console.log(entry.key, entry.value)
}
// Atomic operations
const res = await kv.atomic()
.check({ key: ["users", "user-123"], versionstamp: result.versionstamp })
.set(["users", "user-123"], { ...result.value, name: "Jan Updated" })
.commit()
if (!res.ok) {
console.log("Conflict - someone else modified the data")
}Node.js compatibility
Importing Node.js modules
// Node.js built-in modules
import process from "node:process"
import fs from "node:fs/promises"
import path from "node:path"
import { Buffer } from "node:buffer"
// Usage
console.log(process.env.HOME)
const content = await fs.readFile("./file.txt", "utf-8")
const fullPath = path.join(process.cwd(), "data")npm packages
// Import with npm:
import express from "npm:express@4"
import _ from "npm:lodash"
import { PrismaClient } from "npm:@prisma/client"
// In deno.json (preferred)
{
"imports": {
"express": "npm:express@^4.18.0",
"lodash": "npm:lodash@^4.17.0"
}
}
// Then import normally
import express from "express"Migrating from Node.js
# Run a Node.js project in Deno
DENO_FUTURE=1 deno run --allow-all npm:your-script
# Or use compatibility mode
deno run --node-modules-dir --allow-all main.tsBest practices
Project structure
my-deno-project/
├── deno.json # Configuration
├── deno.lock # Lockfile
├── main.ts # Entry point
├── deps.ts # (optional) Central dependencies
├── src/
│ ├── mod.ts # Public API
│ ├── routes/
│ │ ├── users.ts
│ │ └── posts.ts
│ ├── services/
│ │ └── database.ts
│ └── utils/
│ └── helpers.ts
├── tests/
│ ├── routes_test.ts
│ └── services_test.ts
└── scripts/
└── seed.tsCentralizing dependencies (optional)
// deps.ts - one file with all external dependencies
export { z } from "npm:zod@^3.23.0"
export { Hono } from "npm:hono@^4.0.0"
export { assertEquals, assertThrows } from "jsr:@std/assert"
export { join, dirname } from "jsr:@std/path"
// Usage in other files
import { z, Hono } from "./deps.ts"Type-safe environment variables
// env.ts
import { z } from "npm:zod"
const envSchema = z.object({
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(["development", "production", "test"]).default("development")
})
export const env = envSchema.parse(Deno.env.toObject())FAQ - frequently asked questions
Can Deno replace Node.js?
For new projects - yes. Deno 2.0 is fully compatible with npm and the Node.js API, so migration is possible. For existing large Node.js projects, migration may require some effort.
Why no access to anything by default?
This is a fundamental security principle. A script downloaded from the internet should not automatically have access to your files or network. You must explicitly grant permissions.
Is Deno faster than Node.js?
In most cases Deno and Node.js have similar performance. Bun is faster than both. Deno stands out with its native TypeScript support and built-in tools.
Can I use npm packages in Deno?
Yes! Since Deno 2.0 there is full npm compatibility. Use npm:package-name or configure import maps in deno.json.
What is JSR?
JSR (JavaScript Registry) is a new package registry from the Deno team. It supports TypeScript natively and offers a better DX than npm. You publish packages via deno publish.
Summary
Deno is a modern JavaScript/TypeScript runtime that fixes the fundamental problems of Node.js:
- Security - Granular permissions, sandboxing by default
- TypeScript - Native support without configuration
- Web standards - Compatibility with browser APIs
- Built-in tools - Formatter, linter, test runner, bundler
- npm compatibility - Full compatibility since version 2.0
- Deno Deploy - Edge deployment in 35+ regions
Deno is ideal for new projects, CLI tools, edge functions, and anywhere you care about security and modern DX.