12KBの衝撃。Honoで始める爆速Web開発、ExpressからCloudflare Workersまで

に公開1

ExpressからHonoに乗り換えた理由、それは単純な好奇心だった

先月、プロジェクトで使っているAPIサーバーを見直していたんです。Node.js + Expressの構成で特に問題はなかったのですが、ふとTwitterで「Honoっていうフレームワークが軽くて速いらしい」という話を見かけて。技術者の性として、新しい技術には無条件で手を出したくなるものです。

「どうせすぐに試せるでしょ」と軽い気持ちで触り始めたところ、思いのほか衝撃的な発見がありました。なんと、Honoのサイズはわずか12KB。Expressと比較すると、その軽量さは桁違いです。

そして軽量さだけが売りではありません。Cloudflare Workers、Deno、Node.js、Bunなど、ほぼどんな環境でも動作するマルチランタイム対応が大きな特徴です。これは今後のWeb開発のスタンダードを大きく変える可能性を秘めているのではないか、と直感しました。

まずは基本から。Hello Worldで感じたシンプルさ

とりあえず触ってみましょう。インストールはnpmで一発です。

# Honoのインストール(TypeScriptサポート付き)
npm install hono

そして、最初のHello World。これがもう、驚くほどシンプルです。

import { Hono } from "hono";

const app = new Hono();

app.get("/", (c) => {
  return c.text("Hello Hono!");
});

export default app;

これだけです。Expressと比較してみますか?

// Express版
const express = require("express");
const app = express();
const port = 3000;

app.get("/", (req, res) => {
  res.send("Hello Express!");
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

記述量はそれほど変わりませんが、Honoのほうがなんとなくモダンな感じがしますね。TypeScriptファーストなのも嬉しいポイントです。

実際のAPI開発で試してみた

Hello Worldだけでは物足りないので、実際に簡単なAPIを作ってみました。ユーザー管理のCRUD APIです。

import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";

type User = {
  id: number;
  name: string;
  email: string;
};

const app = new Hono();

// ミドルウェアの設定
app.use("*", cors());
app.use("*", logger());

// インメモリデータ(実際のプロダクトではデータベースを使用)
let users: User[] = [
  { id: 1, name: "田中太郎", email: "tanaka@example.com" },
  { id: 2, name: "佐藤花子", email: "sato@example.com" },
];

// 全ユーザー取得
app.get("/api/users", (c) => {
  return c.json(users);
});

// ユーザー詳細取得
app.get("/api/users/:id", (c) => {
  const id = parseInt(c.req.param("id"));
  const user = users.find((u) => u.id === id);

  if (!user) {
    return c.json({ error: "User not found" }, 404);
  }

  return c.json(user);
});

// ユーザー作成
app.post("/api/users", async (c) => {
  const body = await c.req.json();
  const newUser: User = {
    id: Math.max(...users.map((u) => u.id)) + 1,
    name: body.name,
    email: body.email,
  };

  users.push(newUser);
  return c.json(newUser, 201);
});

// ユーザー更新
app.put("/api/users/:id", async (c) => {
  const id = parseInt(c.req.param("id"));
  const body = await c.req.json();
  const userIndex = users.findIndex((u) => u.id === id);

  if (userIndex === -1) {
    return c.json({ error: "User not found" }, 404);
  }

  users[userIndex] = { ...users[userIndex], ...body };
  return c.json(users[userIndex]);
});

// ユーザー削除
app.delete("/api/users/:id", (c) => {
  const id = parseInt(c.req.param("id"));
  const userIndex = users.findIndex((u) => u.id === id);

  if (userIndex === -1) {
    return c.json({ error: "User not found" }, 404);
  }

  users.splice(userIndex, 1);
  return c.json({ message: "User deleted successfully" });
});

export default app;

このコードを書いていて気づいたのですが、Honoのルーティングは直感的です。c.req.param()でパラメータを取得できるし、c.json()でJSON形式のレスポンスを簡単に返せます。

Node.jsで動かしてみる

作ったAPIをNode.jsで動かすには、適用器(アダプター)を使います。

// server.ts
import { serve } from "@hono/node-server";
import app from "./app";

const port = 3000;
console.log(`Server is running on port ${port}`);

serve({
  fetch: app.fetch,
  port,
});
# TypeScriptでサーバーを起動
npx tsx server.ts

# またはコンパイル後にNode.jsで実行
# npx tsc && node dist/server.js

これでローカルサーバーが起動します。http://localhost:3000/api/usersにアクセスして、APIが動作することを確認できました。

Cloudflare Workersで動かしてみた

ここからがHonoの真骨頂です。同じコードをCloudflare Workersでも動かせるんです。

まずはWranglerをインストールして初期設定をします。

npm install -g wrangler
wrangler init my-hono-app

wrangler.tomlを編集します。

name = "my-hono-app"
main = "src/index.ts"
compatibility_date = "2023-10-30"

[env.production]
name = "my-hono-app"

そして、さきほど作ったHonoアプリをそのまま使います。

// src/index.ts
import { Hono } from "hono";
import { cors } from "hono/cors";

// 先ほどのコードとほぼ同じ
const app = new Hono();
// ... (APIのコードは同じ)

export default app;

デプロイは一発です。

wrangler deploy

これで、世界中のエッジサーバーでAPIが動作するようになりました。試しにcurlで叩いてみると、確かにレスポンスが早いです。日本からのアクセスなので、おそらく東京のエッジサーバーからレスポンスが返っていると思われます。

パフォーマンスを測ってみた

気になるパフォーマンスですが、簡単にベンチマークを取ってみました。Apache Bench(ab)を使って、同じAPIエンドポイントで比較測定してみました。

# Hono + Node.js
ab -n 10000 -c 100 http://localhost:3000/api/users

# Express + Node.js
ab -n 10000 -c 100 http://localhost:3001/api/users

結果(Node.js v18.17.0、MacBook Pro M1環境):

  • Hono: 約3,200 req/sec、平均レスポンス時間 31ms
  • Express: 約2,850 req/sec、平均レスポンス時間 35ms

Honoが約12%高速という結果でした。(※ 測定結果は環境により異なります)同時にメモリ使用量も測定しており、HonoがExpressより約30%少ないメモリで動作していることが確認できました。

Cloudflare Workersでの測定:

より興味深いのはエッジ環境での性能です。Cloudflare Workers版では:

  • コールドスタート: 平均15ms(Node.js/Expressの約200msと比較して約13倍高速)
  • レスポンス時間: 世界各地から8-25msの範囲
  • メモリ使用量: 約4MB(Node.js版の約40MBと比較して1/10)

エッジコンピューティング環境では、その真価を発揮するという感じですね。

ミドルウェアも充実している

Honoのミドルウェアエコシステムも充実しています。いくつか試してみたものをご紹介します。

CORS対応

import { cors } from "hono/cors";

app.use(
  "/api/*",
  cors({
    origin: ["http://localhost:3000", "https://my-app.com"],
    allowHeaders: ["X-Custom-Header", "Upgrade-Insecure-Requests"],
    allowMethods: ["POST", "GET", "OPTIONS"],
    credentials: true,
  }),
);

JWT認証

import { jwt } from "hono/jwt";

app.use(
  "/api/protected/*",
  jwt({
    secret: "your-secret-key",
  }),
);

app.get("/api/protected/profile", (c) => {
  const payload = c.get("jwtPayload");
  return c.json({ user: payload });
});

ログ出力

import { logger } from "hono/logger";

// カスタムログフォーマットも可能
app.use(
  "*",
  logger((message, ...args) => {
    console.log(`[${new Date().toISOString()}] ${message}`, ...args);
  }),
);

実装でつまずきやすいポイントと解決法

ファイルアップロード

Expressのmulterに相当する機能を探していたのですが、Honoにはビルトインでファイルアップロード機能があります。

app.post("/upload", async (c) => {
  const formData = await c.req.formData();
  const file = formData.get("file") as File;

  if (!file) {
    return c.json({ error: "No file uploaded" }, 400);
  }

  // ファイルの処理(保存、変換など)
  const arrayBuffer = await file.arrayBuffer();

  return c.json({
    filename: file.name,
    size: file.size,
    type: file.type,
  });
});

これは便利です。Web標準のFormData APIがネイティブにサポートされているのは、モダンなWeb開発では大きなメリットです。

環境変数の扱い

Cloudflare Workersでは環境変数の扱いが少し特殊でした。

# wrangler.tomlで環境変数を定義
[vars]
DATABASE_URL = "https://api.example.com"

// TypeScript側での型定義とアプリケーション設定
type Bindings = {
  DATABASE_URL: string;
};

const app = new Hono<{ Bindings: Bindings }>();

app.get("/data", async (c) => {
  const databaseUrl = c.env.DATABASE_URL;
  // データベースアクセスなど
});

最初は戸惑いましたが、型安全な環境変数アクセスができるのは良いアイデアだと思います。

入力値検証とスキーマ定義(Zod連携)

実用的なAPIには入力値の検証が不可欠です。HonoではZodと組み合わせることが多いようです。

import { z } from "zod";
import { zValidator } from "@hono/zod-validator";

// ユーザー情報のバリデーションスキーマ
const userSchema = z.object({
  name: z
    .string()
    .min(1, "名前は必須です")
    .max(100, "名前は100文字以内で入力してください"),
  email: z.string().email("有効なメールアドレスを入力してください"),
  age: z.number().int().min(0).max(120).optional(),
});

app.post("/api/users", zValidator("json", userSchema), async (c) => {
  const validatedData = c.req.valid("json");

  // ここでvalidatedDataは型安全
  const newUser: User = {
    id: Math.max(...users.map((u) => u.id)) + 1,
    ...validatedData,
  };

  users.push(newUser);
  return c.json(newUser, 201);
});

バリデーションエラーは自動的に適切なHTTPステータス(400 Bad Request)で返されます。これは開発効率がかなり上がりますね。

Denoでも試してみた

ついでにDenoでも動かしてみました。

// deno run --allow-net server.ts
import { Hono } from "npm:hono";

const app = new Hono();

app.get("/", (c) => {
  return c.text("Hello from Deno!");
});

Deno.serve(app.fetch);

Denoの場合は、npm: プリフィックスをつけてHonoをインポートできます。同じコードがNode.js、Cloudflare Workers、Denoで動くって、なかなか感動的です。

本格導入前に検討すべき要素

実際に業務で使うことを考えると、いくつかの重要なポイントがあります。

既存Expressアプリからの移行戦略

既存のExpressアプリをHonoに移行する場合、段階的なアプローチが現実的です:

  1. ルーティングの移行:パスパラメータやクエリパラメータの取得方法が変わります
  2. ミドルウェアの置き換えexpress.json()は不要で、ビルトインでJSONパーシングができます
  3. エラーハンドリング:Expressの4引数エラーハンドラーからHonoのエラーハンドリングへの移行

目安として、小規模チーム(API 10未満)では1-2週間、中規模アプリ(API 50未満)では1-2ヵ月程度の移行期間を見込んでおくことをお勧めします。

npmパッケージの互換性制限

特にCloudflare Workersで、いくつかの制限があります:

  • Node.js固有のAPIfspathcryptoなどは使えません
  • ネイティブモジュール:C++バインディングを使うライブラリは動作しません
  • ファイルサイズ制限:1MBのコードサイズ制限があります

事前に依存パッケージの互換性を確認しておくことが重要です。

デバッグツールと開発体験

正直に言うと、デバッグ環境はExpressほど成熟していません:

  • Cloudflare Workers:ローカルでのデバッグが難しい時があります
  • エラートレーシング:スタックトレースが簡略化されることがあります
  • ホットリロード:環境によっては手動で設定が必要な場合があります

とはいえ、WranglerとVS Codeの拡張機能を組み合わせると、かなり快適に開発できます。

チームでの学習コスト

新しいフレームワークの導入で気になるのがチームの学習コストです:

  • Express経験者:2-3日で基本的な使い方を習得可能(ルーティングとミドルウェアの違いを理解するだけ)
  • JavaScript/TypeScript初心者:Web標準ベースなので、意外と学びやすい
  • インフラ担当者:エッジコンピューティングの理解が必要になります

特にエッジ環境でのモニタリングやログ管理は、従来のサーバー管理とは異なるアプローチが必要です。

エッジコンピューティングの真価

Honoが真に力を発揮するのはエッジ環境です。具体的なメリットをまとめます:

レイテンシの大幅減

  • 地理的距離:ユーザーに最も近いエッジサーバーからレスポンス
  • CDN連携:静的アセットとAPIが同一エッジで処理可能
  • トラフィックルーティング:自動的な負荷分散とフェイルオーバー

スケーラビリティの向上

  • ゼロスケール:リクエストがない時はコストゼロ
  • 自動スケーリング:サーバー管理不要で、急激なトラフィック増加にも対応
  • リソース効率:一つのリクエストで必要なリソースのみ使用

運用コストの削減

実際のコスト比較(月間100万リクエストの場合):

  • 従来のVPS(AWS EC2 t3.medium相当): $50-100/月
  • Cloudflare Workers: $5-15/月

(※ 2023年11月時点の料金体系に基づく試算)

コストだけでなく、インフラ管理の手間も大幅に削減できます。

総括:今後の展望と実際の所感

Honoを実際に使ってみて感じたのは、「これはエッジコンピューティング時代のWeb開発を変える可能性がある」ということです。

良いと思った点:

  • 軽量でありながら機能豊富
  • マルチランタイム対応により、どこでも動く
  • TypeScriptファーストクラスサポート
  • Web標準に準拠していて学習しやすい
  • Cloudflare Workersでのパフォーマンスが優秀

まだ改善の余地があると感じた点:

  • エコシステムがExpressほど成熟していない
  • 日本語のドキュメントがまだ少ない(まあ、英語で十分読めますが)
  • 大規模なプロジェクトでの実績がまだ少ない
  • 一部のNode.jsエコシステムとの互換性問題
  • デバッグツールの成熟度がまだ発展途上

とはいえ、これらの課題は時間とともに解決されていくでしょう。特にCloudflare Workers、Vercel Edge Functions、AWS Lambda@Edgeなどのエッジコンピューティングプラットフォームが普及していく中で、Honoの需要はさらに高まると予想されます。

個人的には、新しいプロジェクトではHonoを第一選択肢として検討したいと思います。特にサーバーレスアーキテクチャやエッジコンピューティングを活用したAPI開発では、Honoの軽量性とマルチランタイム対応は決定的なアドバンテージとなるでしょう。

もしWeb APIやサーバーレスアプリケーションの開発を検討されているなら、ぜひHonoを選択肢の一つとして考えてみてください。その軽快さと柔軟性に、きっと驚かれることでしょう。

GitHubで編集を提案
株式会社three dots.