Deno 2.0リリース - Node.js互換性の大幅強化

2025.12.18

公式ドキュメント

この記事の要点

• 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 installnode_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.0Node.js
TypeScriptネイティブサポート要トランスパイラ
セキュリティ明示的な権限制御全権限デフォルト
パッケージ管理URL/npm/jsrnpm
設定ファイル最小限多数必要
Web標準APIfetch等標準サポートv18以降で追加
エコシステム急成長中巨大・成熟
組み込みKVDeno KVで標準対応なし(外部依存)
単一実行ファイルdeno compile要pkg等
LTS有り(Deno 2.0以降)有り

Deno 1.x系からの主な変更点

項目Deno 1.xDeno 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.jsonimportsに集約し、ソース内では論理名で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プロジェクトの移行手順

  1. Denoをインストールして、deno --versionで2.0以降を確認
  2. そのまま実行してみる: deno run --allow-all index.js
  3. 失敗箇所を特定し、互換性問題を修正
  4. deno.jsonを追加してタスク・importマップを整備
  5. 段階的にimportを書き換え(CommonJS → ESM、相対パス → import map)
  6. CIにdeno fmt/deno lint/deno testを追加
  7. 権限を段階的に絞る
# ステップ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のリリースでその理想と現実のバランスが取れたと言えます。

参考リンク

この技術を体系的に学びたいですか?

未来学では東証プライム上場企業のITエンジニアが24時間サポート。月額24,800円から、退会金0円のオンラインIT塾です。

メールで無料相談する
← 一覧に戻る