この記事の要点
• Deno 2.0でnpm完全互換・package.json対応・LTS導入を実現
• Node.jsの作者Ryan Dahl氏が「反省点を活かして」開発したランタイム
• セキュリティファーストの設計とTypeScriptネイティブサポートが特徴
• 既存のNode.jsプロジェクトからの移行ハードルが大幅に低下
Deno 2.0がついにリリース
2024年10月、Deno 2.0が正式リリースされました。Node.jsとの互換性を大幅に強化し、実用性が飛躍的に向上しています。Ryan Dahl氏が「Node.jsの反省点を活かして作った」Denoが、ついに本格的な選択肢になりました。
Denoとは: Node.jsの作者Ryan Dahl氏が開発したJavaScript/TypeScriptランタイム。セキュリティ、TypeScriptネイティブサポート、モダンなWeb標準APIが特徴です。
背景 - なぜDeno 2.0が重要なのか
Denoは2018年に初公開されて以来、「Node.jsで後悔している10のこと」というRyan Dahl氏の有名な講演を出発点として、モダンなJavaScriptランタイムの理想を追求してきました。しかし、Deno 1.x系ではnpmエコシステムとの互換性が限定的であり、既存のNode.jsプロジェクトを移行するハードルが高いという課題がありました。
Deno 2.0ではこの課題に真正面から取り組み、以下の方針転換を行いました。
- npm完全互換:
npm:指定子による透過的なnpmパッケージ利用 - package.json対応: 既存Node.jsプロジェクトをそのまま実行
- node_modules対応: 互換性のために伝統的な構造も受け入れる
- LTSリリース: エンタープライズが求める長期サポート
これにより、「セキュリティとモダンさ」という理想を維持しつつ、実務で使える選択肢として成熟しました。
主要な新機能
1. npm完全互換
npmパッケージをそのまま使用できるようになりました。npm:スキーマを使って直接インポートできます。
// npmパッケージを直接インポート
import express from "npm:express@4";
import { z } from "npm:zod";
const app = express();
app.get("/", (req, res) => {
res.json({ message: "Hello from Deno!" });
});
app.listen(3000);
ほとんどのnpmパッケージが動作し、Node.jsからの移行が容易になりました。
2. package.jsonサポート
package.jsonがあれば、従来通りのnpm installやnode_modulesも使用可能に。
# 既存のNode.jsプロジェクトをDenoで実行
deno run --allow-net --allow-read app.js
# package.jsonのscriptsも実行可能
deno task start
3. LTS(Long Term Support)の導入
企業での採用を見据えて、LTSバージョンが導入されました。安定版の長期サポートにより、本番環境での利用が現実的になりました。
4. ワークスペースと複数プロジェクト管理
モノレポ構成をサポート。deno.jsonでワークスペースを定義できます。
// deno.json
{
"workspace": ["./packages/api", "./packages/web", "./packages/shared"]
}
5. JSRによるパッケージ公開
JSR(JavaScript Registry)が登場。TypeScriptをそのまま公開でき、npmとDenoの両方で使えるパッケージを配布できます。
// JSRからのインポート
import { assertEquals } from "jsr:@std/assert";
// 公開は簡単
// deno publish
セキュリティモデル
Denoの大きな特徴であるセキュリティモデルも進化しています。
# 必要な権限のみを明示的に許可
deno run --allow-net=api.example.com --allow-read=./data app.ts
# 全権限を許可(開発時のみ推奨)
deno run -A app.ts
| 権限フラグ | 説明 |
|---|---|
--allow-net | ネットワークアクセス |
--allow-read | ファイル読み取り |
--allow-write | ファイル書き込み |
--allow-env | 環境変数アクセス |
--allow-run | サブプロセス実行 |
きめ細かい権限制御
Deno 2.0ではホワイトリスト的な指定がさらに強化され、ドメイン・ポート単位、パス単位で権限を絞り込めます。
# 特定ホスト・ポートのみ許可
deno run \
--allow-net=api.example.com:443,cdn.example.com:443 \
--allow-read=./config,./data \
--allow-write=./logs \
--allow-env=DATABASE_URL,API_KEY \
app.ts
この仕組みはCI/CD環境での「最小権限の原則」実現に非常に有効です。攻撃者がサプライチェーン攻撃でnpmパッケージを汚染しても、権限がなければファイル窃取や外部通信ができません。
開発ツールの充実
Deno 2.0では開発体験も大幅に改善されています。
組み込みツール
# フォーマッター
deno fmt
# リンター
deno lint
# テストランナー
deno test
# ベンチマーク
deno bench
# ドキュメント生成
deno doc
# バンドラー
deno compile # 単一実行ファイルを生成
TypeScriptネイティブ
設定不要でTypeScriptがそのまま動作。tsconfig.jsonの設定に悩む必要がありません。
// app.ts - そのまま実行可能
interface User {
id: number;
name: string;
}
const user: User = { id: 1, name: "田中" };
console.log(user);
実践的なサンプルコード
REST APIサーバーの実装例
Deno標準のDeno.serveとHonoを組み合わせて、型安全なREST APIを構築してみましょう。
// server.ts
import { Hono } from "npm:hono";
import { zValidator } from "npm:@hono/zod-validator";
import { z } from "npm:zod";
interface Todo {
id: string;
title: string;
done: boolean;
createdAt: string;
}
const todos = new Map<string, Todo>();
const createTodoSchema = z.object({
title: z.string().min(1).max(200),
});
const app = new Hono();
app.get("/todos", (c) => {
return c.json([...todos.values()]);
});
app.get("/todos/:id", (c) => {
const id = c.req.param("id");
const todo = todos.get(id);
if (!todo) return c.json({ error: "Not Found" }, 404);
return c.json(todo);
});
app.post(
"/todos",
zValidator("json", createTodoSchema),
(c) => {
const { title } = c.req.valid("json");
const todo: Todo = {
id: crypto.randomUUID(),
title,
done: false,
createdAt: new Date().toISOString(),
};
todos.set(todo.id, todo);
return c.json(todo, 201);
},
);
app.patch("/todos/:id/toggle", (c) => {
const id = c.req.param("id");
const todo = todos.get(id);
if (!todo) return c.json({ error: "Not Found" }, 404);
todo.done = !todo.done;
return c.json(todo);
});
app.delete("/todos/:id", (c) => {
const id = c.req.param("id");
todos.delete(id);
return c.body(null, 204);
});
Deno.serve({ port: 8000 }, app.fetch);
起動は以下のコマンド。
deno run --allow-net server.ts
KVストアでの永続化
Deno 2.0はDeno KV(組み込みKey-Valueストア)が安定化しました。SQLiteベースで、ファイルベースにもDeno Deployの分散環境にも対応します。
// kv-example.ts
const kv = await Deno.openKv();
interface User {
id: string;
name: string;
email: string;
}
// 書き込み
const user: User = {
id: crypto.randomUUID(),
name: "Alice",
email: "alice@example.com",
};
await kv.set(["users", user.id], user);
// 読み取り
const result = await kv.get<User>(["users", user.id]);
console.log(result.value);
// 一覧取得
for await (const entry of kv.list<User>({ prefix: ["users"] })) {
console.log(entry.key, entry.value);
}
// アトミックオペレーション
await kv.atomic()
.check({ key: ["counter"], versionstamp: null })
.set(["counter"], 1)
.commit();
// インクリメント(Sum操作)
await kv.atomic()
.sum(["counter"], 1n)
.commit();
ファイルシステムアクセスとストリーム処理
// csv-processor.ts
import { parse } from "jsr:@std/csv";
const file = await Deno.open("./data/large.csv");
const stream = file.readable
.pipeThrough(new TextDecoderStream())
.pipeThrough(new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk);
},
}));
const reader = stream.getReader();
let buffer = "";
let count = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += value;
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (line.trim()) count++;
}
}
console.log(`Processed ${count} records`);
Node.jsとの比較
| 機能 | Deno 2.0 | Node.js |
|---|---|---|
| TypeScript | ネイティブサポート | 要トランスパイラ |
| セキュリティ | 明示的な権限制御 | 全権限デフォルト |
| パッケージ管理 | URL/npm/jsr | npm |
| 設定ファイル | 最小限 | 多数必要 |
| Web標準API | fetch等標準サポート | v18以降で追加 |
| エコシステム | 急成長中 | 巨大・成熟 |
| 組み込みKV | Deno KVで標準対応 | なし(外部依存) |
| 単一実行ファイル | deno compile | 要pkg等 |
| LTS | 有り(Deno 2.0以降) | 有り |
Deno 1.x系からの主な変更点
| 項目 | Deno 1.x | Deno 2.0 |
|---|---|---|
| npm対応 | 実験的(npm:指定子) | 安定・完全互換 |
| package.json | 限定的 | 完全対応 |
| node_modules | 基本的に使わない | 互換モードで利用可 |
| LTS | なし | あり |
| ワークスペース | 限定的 | モノレポ完全対応 |
deno install | グローバルのみ | ローカル依存もサポート |
フレームワーク対応状況
主要なフレームワークのDeno対応状況です。
- Fresh: Deno公式のフルスタックフレームワーク(Islands Architecture)
- Hono: 高速なWebフレームワーク(Deno完全対応)
- Oak: Express風のミドルウェアフレームワーク
- Express: npm互換で動作
- Next.js: 部分的にサポート
// Honoの例
import { Hono } from "npm:hono";
const app = new Hono();
app.get("/", (c) => c.json({ message: "Hello Hono!" }));
Deno.serve(app.fetch);
Freshによるフルスタック開発
// routes/index.tsx
import { Handlers, PageProps } from "$fresh/server.ts";
interface Data {
message: string;
timestamp: string;
}
export const handler: Handlers<Data> = {
GET(_req, ctx) {
return ctx.render({
message: "Hello Fresh",
timestamp: new Date().toISOString(),
});
},
};
export default function Home({ data }: PageProps<Data>) {
return (
<main>
<h1>{data.message}</h1>
<p>Rendered at: {data.timestamp}</p>
</main>
);
}
ベストプラクティス
1. 権限は最小限に明示
-A(全権限許可)は開発時のみに留め、本番環境では必要な権限のみを渡しましょう。権限セットをタスクに定義しておくと運用が楽になります。
// deno.json
{
"tasks": {
"dev": "deno run --allow-net --allow-read --allow-env --watch src/main.ts",
"start": "deno run --allow-net=0.0.0.0:8000 --allow-read=./public --allow-env=DATABASE_URL src/main.ts",
"test": "deno test --allow-read --allow-env"
}
}
2. importマップでバージョン集約
依存関係のバージョンはdeno.jsonのimportsに集約し、ソース内では論理名でimportします。
{
"imports": {
"hono": "npm:hono@4",
"zod": "npm:zod@3",
"@std/assert": "jsr:@std/assert@1",
"@std/path": "jsr:@std/path@1"
}
}
import { Hono } from "hono";
import { z } from "zod";
3. JSRを優先、必要に応じてnpm
JSRネイティブのパッケージは型がそのまま配信されるためTypeScript連携が滑らかです。無い場合のみnpm:を利用しましょう。
4. deno fmt / deno lint をCIで実行
組み込みツールを使えばPrettier/ESLintの設定に悩む必要がありません。
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- run: deno fmt --check
- run: deno lint
- run: deno check src/**/*.ts
- run: deno test --allow-read
5. deno compile で配布
Denoは単一バイナリへのコンパイルが可能です。CLIツールの配布に便利です。
deno compile --allow-net --output mycli src/cli.ts
./mycli --help
注意点・落とし穴
Node.js互換のエッジケース
多くのnpmパッケージが動作しますが、以下は注意が必要です。
- ネイティブアドオン: C++バインディングを使うパッケージ(sharp、canvasなど)は動作しない場合がある
__dirname/__filename: CommonJS前提のコードはimport.meta.urlへ書き換えが必要process.*: Deno 2.0で互換性は向上したが、完全ではない- Workerスレッド:
node:worker_threadsは部分対応
パーミッションプロンプト
対話式実行時、未許可の操作があるとプロンプトが出ますが、CIでは必ずフラグで明示しましょう。
依存のバージョンロック
Deno 2.0ではdeno.lockが自動生成されます。これをコミットしてビルドの再現性を確保してください。
import mapsと相対パス
import mapsの論理名と相対パスを混在させるとリファクタリング時に混乱するため、どちらかに統一することを推奨します。
導入・移行手順
新規プロジェクトの場合
# Denoインストール (macOS/Linux)
curl -fsSL https://deno.land/install.sh | sh
# または Homebrew
brew install deno
# バージョン確認
deno --version
# プロジェクト初期化
deno init my-app
cd my-app
deno task dev
既存Node.jsプロジェクトの移行手順
- Denoをインストールして、
deno --versionで2.0以降を確認 - そのまま実行してみる:
deno run --allow-all index.js - 失敗箇所を特定し、互換性問題を修正
deno.jsonを追加してタスク・importマップを整備- 段階的にimportを書き換え(CommonJS → ESM、相対パス → import map)
- CIに
deno fmt/deno lint/deno testを追加 - 権限を段階的に絞る
# ステップ2: そのまま実行
deno run -A --unstable-sloppy-imports index.js
# ステップ4: package.jsonをベースにdeno.jsonを作成
deno init
パフォーマンス/ベンチマーク
Deno 2.0では起動時間とHTTPスループットが改善されています。
起動時間(Hello Worldスクリプト)
| ランタイム | 起動時間 |
|---|---|
| Deno 2.0 | 約30ms |
| Deno 1.40 | 約45ms |
| Node.js 20 | 約40ms |
| Bun 1.1 | 約15ms |
HTTPサーバー(シンプルなJSON応答、同一マシン)
| ランタイム | req/sec(概算) |
|---|---|
Deno 2.0 (Deno.serve) | 約12万 |
| Deno 1.40 | 約9万 |
Node.js 20 (http) | 約8万 |
| Bun 1.1 | 約22万 |
(数値は環境により大きく変動するため目安。実運用では自環境でベンチマークを取ること。)
Deno KVのレイテンシ
Deno KVはSQLiteベースのローカル動作でサブミリ秒のgetが可能です。Deno Deployの分散モードでは、eventual consistencyを受け入れる代わりにグローバルスケールを実現します。
FAQ
Q1: Deno 2.0でNode.jsの置き換えは現実的ですか?
A: 新規プロジェクトでは十分現実的です。既存の大規模Node.jsアプリは、依存パッケージのネイティブアドオン有無やビルドパイプラインとの相性を確認した上で、段階的に移行するのが安全です。
Q2: npmとjsrはどちらを使うべきですか?
A: 型安全性と配信速度の観点からJSRが優先される場面が増えていますが、エコシステムの広さではnpmが圧倒的です。まずJSRを探し、なければnpmにフォールバックする方針がおすすめです。
Q3: Deno DeployとCloudflare Workersの違いは?
A: Deno DeployはDenoランタイム互換で、File System APIなど多くのDeno APIが使えます。Cloudflare WorkersはV8 Isolateベースで起動が極めて速い反面、利用できるAPIに制約があります。用途に応じて選びましょう。
Q4: TypeScriptの型チェックはいつ行われますか?
A: デフォルトではdeno run時に型チェックはスキップされ、実行速度が優先されます。CIではdeno checkを明示的に実行することを推奨します。
Q5: node_modulesは必要ですか?
A: 不要です。Denoはグローバルキャッシュから依存を読み込みます。ただしnpm互換のために--node-modules-dirオプションで生成することも可能です。
まとめ
Deno 2.0は、Node.js互換性の大幅強化により実用的な選択肢になりました。セキュリティ、TypeScriptネイティブ、モダンなAPIなど、多くの利点を持っています。新規プロジェクトや、セキュリティを重視する案件では、積極的に検討する価値があります。
採用判断の指針
- 積極採用: セキュリティ要件が厳しい新規プロジェクト、CLIツール、エッジ環境(Deno Deploy)
- 検討推奨: TypeScript中心の中規模Webアプリ、モノレポ構成のチーム
- 慎重判断: ネイティブアドオンに強く依存するNode.jsアプリ、成熟したビルドパイプラインを持つ既存大規模システム
Denoは「Node.jsの後悔」を丁寧に解消しようとするランタイムであり、2.0のリリースでその理想と現実のバランスが取れたと言えます。