Deno 2.0でクロスプラットフォームアプリを構築する:CLIツール・HTTPサーバー・npm連携

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

クイックスタート:5分で動かす

Denoの名前は知っていても、なんとなく後回しにしていた方も多いはず。Deno 2.0は、そんな方々がついに「これだ!」と感じるバージョンです。私自身、ビルドステップなしでTypeScriptスクリプトを実行する方法を探しているときに試してみました。驚いたのは、インストールから実行まで3分もかからず、tsconfigに一切触れなくてよかったことです。

コマンド一発でDenoをインストールできます。npmもnode_modulesも不要です:

# macOS / Linux
curl -fsSL https://deno.land/install.sh | sh

# Windows (PowerShell)
irm https://deno.land/install.ps1 | iex

# パッケージマネージャーでインストール
brew install deno          # macOS Homebrew
winget install DenoLand.Deno  # Windows

インストールを確認します:

deno --version
# deno 2.0.x (release, ...)

最初のTypeScriptファイルを実行してみましょう。コンパイラも設定ファイルも不要です:

// hello.ts
const name: string = "Deno 2.0";
console.log(`こんにちは、${name}!`);
deno run hello.ts
# こんにちは、Deno 2.0!

たったこれだけです。TypeScriptがそのまま動きます。典型的なNode + TypeScriptのセットアップと比べてみましょう:npm initを実行し、ts-nodetsxをインストールし、tsconfig.jsonを書いて、ようやくファイルを実行できます。Denoはこれらすべてをスキップします。

深掘り:CLIツールとHTTPサーバーを構築する

CLIツールを作る

CLIスクリプトの作成こそ、Denoが真価を発揮する場面です。同僚はNode、npm、あるいは任意のランタイムをインストールする必要がありません。単一バイナリにコンパイルして配布するだけです。「自分の環境では動くのに」という悩みから一気に解放されます。

ファイルを読み込んで単語の出現頻度を数えるCLIツールです:

// wordcount.ts
const args = Deno.args;

if (args.length === 0) {
  console.error("使い方: deno run --allow-read wordcount.ts <ファイル名>");
  Deno.exit(1);
}

const filename = args[0];
const text = await Deno.readTextFile(filename);

const words = text.toLowerCase().match(/\b\w+\b/g) ?? [];
const freq: Record<string, number> = {};

for (const word of words) {
  freq[word] = (freq[word] ?? 0) + 1;
}

const sorted = Object.entries(freq)
  .sort((a, b) => b[1] - a[1])
  .slice(0, 10);

console.log("出現頻度トップ10:");
for (const [word, count] of sorted) {
  console.log(`  ${word}: ${count}`);
}
deno run --allow-read wordcount.ts README.md

--allow-readフラグは意図的なものです。Denoのセキュリティモデルは明示的で、スクリプトはあなたが許可しない限り、ファイル、ネットワーク、環境変数にアクセスできません。最初は少し面倒に感じるかもしれません。しかし、サードパーティのスクリプトを本番環境で実行する際に、外部との通信やファイルシステムの変更ができないことを確認できるのは非常に心強いです。許可されていることが明確なリストで把握できるのです。

単一バイナリにコンパイルする

CLIツールが完成したら、スタンドアロンの実行ファイルにコンパイルします。シンプルなツールでも出力ファイルは通常60〜80MBほどになりますが、これはDenoランタイムが内包されているためです。

# 現在のプラットフォーム向けにコンパイル
deno compile --allow-read wordcount.ts -o wordcount

# 他のプラットフォーム向けにクロスコンパイル
deno compile --target x86_64-pc-windows-msvc --allow-read wordcount.ts -o wordcount.exe
deno compile --target x86_64-apple-darwin --allow-read wordcount.ts -o wordcount-mac
deno compile --target x86_64-unknown-linux-gnu --allow-read wordcount.ts -o wordcount-linux

このバイナリをどのマシンに置いても動きます。ランタイムのインストールは不要です。

HTTPサーバーを作る

Deno 2.0には本番対応のHTTPサーバーが標準搭載されています。多くのユースケースでは、ExpressやFastifyは不要です:

// server.ts
const PORT = 8000;

const handler = (req: Request): Response => {
  const url = new URL(req.url);

  if (url.pathname === "/") {
    return new Response("Denoサーバーへようこそ!", {
      headers: { "Content-Type": "text/plain" },
    });
  }

  if (url.pathname === "/health") {
    return Response.json({ status: "ok", timestamp: new Date().toISOString() });
  }

  if (url.pathname === "/echo" && req.method === "POST") {
    const body = req.body;
    return new Response(body, {
      headers: { "Content-Type": req.headers.get("Content-Type") ?? "text/plain" },
    });
  }

  return new Response("Not Found", { status: 404 });
};

console.log(`サーバーが http://localhost:${PORT} で起動しました`);
Deno.serve({ port: PORT }, handler);
deno run --allow-net server.ts

# テスト
curl http://localhost:8000/health
# {"status":"ok","timestamp":"2026-05-21T10:00:00.000Z"}

ハンドラーは標準のRequestResponseオブジェクトを使用します。これはブラウザが公開しているWeb APIとまったく同じものです。Fetch APIを使ったことがあれば、すぐに馴染めるでしょう。

応用:npmパッケージを統合する

npmパッケージを直接使う

Deno 2.0はnpmをファーストクラスとして扱います。npm:スペシファイアを使ってパッケージをインポートでき、node_modulesディレクトリは不要です:

// npm: スペシファイアでnpmパッケージを使用する
import express from "npm:express@4";
import chalk from "npm:chalk@5";

const app = express();

app.get("/", (req, res) => {
  console.log(chalk.green("リクエストを受信しました!"));
  res.json({ message: "Deno + Expressからこんにちは!" });
});

app.listen(3000, () => {
  console.log(chalk.blue("ポート3000でサーバーが起動しました"));
});
deno run --allow-net --allow-read --allow-env app.ts

deno.json設定ファイルを用意する

実際のプロジェクトではdeno.jsonを用意すると便利です。インポートエイリアスやタスクスクリプトを一元管理でき、package.jsonに似た役割を果たします:

{
  "imports": {
    "@std/fs": "jsr:@std/fs@1",
    "@std/path": "jsr:@std/path@1",
    "zod": "npm:zod@3",
    "chalk": "npm:chalk@5"
  },
  "tasks": {
    "start": "deno run --allow-net --allow-read server.ts",
    "dev": "deno run --watch --allow-net --allow-read server.ts",
    "build": "deno compile --allow-net --allow-read server.ts -o myserver",
    "test": "deno test --allow-read"
  },
  "compilerOptions": {
    "strict": true
  }
}

npmスクリプトと同じ方法でタスクを実行できます:

deno task dev    # ホットリロードで開発
deno task build  # バイナリにコンパイル
deno task test   # テストを実行

実践例:Zodでバリデーション付きAPIを作る

Zodを使って入力バリデーションを実装した、より実践的なサーバーの例です。実際のデプロイに近い構成になっています:

// api.ts
import { z } from "zod";

const UserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().min(18),
});

const handler = async (req: Request): Promise<Response> => {
  if (req.method === "POST" && new URL(req.url).pathname === "/users") {
    try {
      const body = await req.json();
      const user = UserSchema.parse(body);
      // 実際のアプリではここでデータベースに保存する
      return Response.json({ success: true, user }, { status: 201 });
    } catch (err) {
      if (err instanceof z.ZodError) {
        return Response.json(
          { success: false, errors: err.errors },
          { status: 400 }
        );
      }
      return Response.json({ success: false, message: "JSONが無効です" }, { status: 400 });
    }
  }

  return new Response("Not Found", { status: 404 });
};

Deno.serve({ port: 8000 }, handler);
# 有効なリクエスト
curl -X POST http://localhost:8000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "[email protected]", "age": 25}'

# 無効なリクエスト — Zodがキャッチする
curl -X POST http://localhost:8000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "", "email": "not-an-email", "age": 15}'

実践的なヒント

開発時は–watchを使う

--watchフラグはファイルの変更を検知してスクリプトを自動再起動します。nodemonなどの追加インストールは不要です:

deno run --watch --allow-net server.ts

パーミッションモデルを早めに理解する

開発の初期段階では--allow-allを使えば、フラグを推測する手間が省けます。デプロイ前にしっかり制限しましょう:

# 開発:全権限を開放
deno run --allow-all server.ts

# 本番:必要な権限のみ
deno run --allow-net=:8000 --allow-read=./data server.ts

テストは標準搭載

Denoには独自のテストランナーが内蔵されています。Jest、Mocha、設定ファイルは一切不要です:

// wordcount_test.ts
import { assertEquals } from "jsr:@std/assert";

Deno.test("単語を正しくカウントできる", () => {
  const text = "hello world hello";
  const words = text.match(/\b\w+\b/g) ?? [];
  const freq: Record<string, number> = {};
  for (const word of words) freq[word] = (freq[word] ?? 0) + 1;
  assertEquals(freq["hello"], 2);
  assertEquals(freq["world"], 1);
});
deno test wordcount_test.ts

オフライン環境向けに依存パッケージをキャッシュする

Denoは初回実行時に依存パッケージをダウンロードし、ローカルにキャッシュします。CIパイプラインや制限された環境では、事前にすべてキャッシュしておくと便利です:

# deno.jsonから全依存パッケージをインストール・キャッシュ
deno install

# またはロックファイルを使って特定のエントリーファイルをキャッシュ
deno cache --lock=deno.lock server.ts

本番環境へのデプロイ

コンパイルしたバイナリはどのLinuxサーバーでも動作します。対象マシンにDenoのインストールは不要です:

# ビルド
deno compile --target x86_64-unknown-linux-gnu \
  --allow-net --allow-read \
  server.ts -o myserver

# デプロイ
scp myserver user@your-server:/opt/myapp/
ssh user@your-server '/opt/myapp/myserver'

もしくはGitHubにプッシュするだけで、Deno Deployが残りの設定を引き受けてくれます。サーバー設定もDockerも不要で、すぐにライブURLが手に入ります。

Node.jsから移行する場合、完全に慣れるまで1週間ほどかかると思っておきましょう。最初は明示的なパーミッションが煩わしく感じるかもしれません。しかし数日もすれば自然と身につき、レビューするすべてのスクリプトがアクセス可能な項目を明確に宣言しているありがたさを実感できるはずです。まずは小さく始めましょう。内部で使うCLIツールをひとつ作るだけで、Denoの全体像の80%が掴めます。

Share: