はじめに
現在、個人開発でTypeScriptを使ったWebアプリケーションを開発しています。
バックエンドには、HonoというWebフレームワークを採用しました。
開発ではAIを積極的に活用しており、APIの実装や認証処理、データベースとの連携などを進めています。
しかし、開発を進める中で、ふと疑問が浮かびました。
「そもそも、Honoの何が良いのだろう?」
Honoを採用した理由として、軽量であることやTypeScriptとの相性が良いことなどは把握していました。
ただ、実際にコードを書いている中で、その特徴やメリットを十分に理解できているかと聞かれると、少し自信がありませんでした。
特にAIを活用した開発では、コードの実装を効率的に進められる一方で、利用している技術の仕組みや採用理由を十分に理解しないまま開発が進んでしまうこともあると感じています。
そこで今回は、開発の途中ではありますが、Honoの特徴や魅力について改めて調べてみることにしました。
この記事では、Honoの基本的な特徴から、サンプルコードを使った実装方法、Expressとの違いまで整理していきます。
※ 記事内のコードは学習用に作成したサンプルです。
この記事の対象者
- Honoに興味がある方
- Honoの特徴やメリットを知りたい方
- HonoとExpressの違いを知りたい方
- TypeScriptでバックエンド開発を始めたい方
- AIを活用した開発を進める中で、利用技術への理解を深めたい方
Honoとは?
Honoは、JavaScript・TypeScriptでWeb APIやWebアプリケーションを開発するためのWebフレームワークです。
読み方は「ほの」。
日本発のオープンソースプロジェクトとして知られています。
主に以下のような用途で利用できます。
- REST APIの開発
- バックエンドアプリケーションの構築
- サーバーレス環境でのAPI実装
- エッジ環境でのアプリケーション開発
Honoの特徴として、よく挙げられるのが以下の5つです。
- 軽量でシンプル
- 外部ライブラリへの依存が少ない
- TypeScriptとの親和性が高い
- ミドルウェアやルーティングを扱いやすい
- 複数のJavaScript実行環境に対応している
それぞれ詳しく見ていきます。
Honoの特徴と魅力
1. 軽量でシンプルな構成
まず魅力として挙げられるのが、少ないコードでAPIを実装できることです。
例えば、以下のコードを見てください。
import { Hono } from "hono";
// Honoアプリケーションを作成
const app = new Hono();
// GET /hello にアクセスされた際の処理を定義
app.get("/hello", (c) => {
// JSON形式でレスポンスを返す
return c.json({
message: "Hello Hono!",
});
});
export default app;
これだけで、GET /helloというAPIを定義できます。
APIにアクセスすると、以下のJSONが返されます。
{
"message": "Hello Hono!"
}
コードの解説
| コード | 役割 |
|---|---|
new Hono() |
Honoのアプリケーションを作成 |
app.get() |
GETリクエストの処理を定義 |
"/hello" |
APIのエンドポイント |
c |
リクエストやレスポンスを扱うContext |
c.json() |
JSON形式のレスポンスを返す |
非常にシンプルな構成です。
特に、c.json()だけでJSONレスポンスを返せる点は分かりやすいと感じました。
また、Honoは依存関係を抑えた軽量な設計になっており、必要な機能を組み合わせながら開発できます。
ただし、軽量だからといって、どのようなアプリケーションでも必ず高速になるわけではありません。
実際のパフォーマンスは、データベースへのアクセスや外部APIとの通信などにも左右されます。
Honoの魅力は、必要なAPIをシンプルな構成で実装できることにあると感じました。
2. 外部ライブラリへの依存が少ない
Honoについて調べる中で、もう一つ興味深い特徴がありました。
それは、外部ライブラリへの依存を極力抑えた設計になっていることです。
Hono本体は、通常の実行時依存パッケージを持たない構成になっています。
2-1. 依存ライブラリとは?
依存ライブラリとは、アプリケーションやフレームワークが動作するために必要とする外部のプログラムのことです。
例えば、以下のような構成を考えます。
アプリケーション
│
▼
ライブラリA
│
├── ライブラリB
│
└── ライブラリC
│
└── ライブラリD
この場合、アプリケーションが直接利用しているのはライブラリAだけです。
しかし、ライブラリAが別のライブラリに依存しているため、実際には複数のパッケージが必要になります。
このように、ライブラリが別のライブラリに依存することを「間接依存」と呼びます。
2-2. なぜ依存ライブラリが少ないと良いのか?
依存ライブラリが少ないことには、主に2つのメリットがあります。
① セキュリティリスクを抑えやすい
外部ライブラリに脆弱性が見つかった場合、そのライブラリを利用しているアプリケーションにも影響する可能性があります。
また、悪意のあるコードがパッケージの配布経路などを通じて混入する、サプライチェーン攻撃というリスクも存在します。
依存ライブラリが少なければ、こうしたリスクにさらされる経路を減らしやすくなります。
ただし、依存関係が少ないだけでセキュリティが保証されるわけではありません。
Hono本体の脆弱性や、アプリケーション側の実装不備などには引き続き注意が必要です。
② ライブラリの管理負担を減らせる
依存ライブラリが増えると、それぞれのアップデートや互換性の確認が必要になります。
例えば、ライブラリAを更新した際に、依存しているライブラリBとの互換性に問題が発生することも考えられます。
依存関係が少なければ、こうした管理の負担を抑えやすくなります。
2-3. 実際にHonoの依存関係を確認してみた
Honoは外部ライブラリへの依存が少ないと聞いたため、実際にnpmコマンドを使って確認してみました。
まず、Honoの通常の実行時依存を確認しました。
npm view hono dependencies
実行結果:
(出力なし)
Hono 4系についても確認しました。
npm view hono@4 dependencies
こちらも出力はありませんでした。
続いて、実際に使用しているHonoのバージョンを確認しました。
npm ls hono
実行結果:
backend@0.1.0
├─┬ @hono/node-server@2.1.3
│ └── hono@4.13.13 deduped
└── hono@4.13.13
今回の環境では、Hono 4.13.13を利用していることが分かりました。
また、Node.jsでHonoを動作させるために、@hono/node-serverも利用しています。
なお、dedupedは依存パッケージが重複しないよう、npmによって共通化されていることを示しています。
さらに、実際に使用しているHono 4.13.13についても確認しました。
npm view hono@4.13.13 dependencies --json
実行結果:
(出力なし)
これらの結果から、今回確認したHono 4.13.13には、通常の実行時依存パッケージが定義されていないことが分かりました。
ただし、Hono本体の依存関係が少ないことと、アプリケーション全体の依存関係が少ないことは別です。
実際の開発では、実行環境に応じたアダプターや、データベース操作・認証などのためのライブラリを組み合わせる必要があります。
また、依存パッケージが少ないことで、外部ライブラリに起因するセキュリティリスクを減らしやすくなりますが、アプリケーションの安全性が保証されるわけではありません。
2-4. Honoなら追加ライブラリは不要なのか?
ここは注意が必要です。
Hono本体の依存関係が少ないからといって、アプリケーション開発で追加ライブラリが不要になるわけではありません。
例えば、以下のような機能を実装する場合は、別途ライブラリを利用することがあります。
| 機能 | 利用するライブラリの例 |
|---|---|
| データベース操作 | Drizzle ORM |
| 入力値の検証 | Zod |
| JWTの検証 | 認証ライブラリ |
| Node.jsでのサーバー起動 | @hono/node-server |
つまり、Honoはすべての機能を標準で提供するフレームワークではなく、必要な機能を組み合わせながら開発する設計になっています。
依存関係を抑えた軽量な構成を維持しつつ、必要に応じて機能を追加できる点もHonoの魅力だと感じました。
3. TypeScriptとの相性が良い
次に、TypeScriptとの親和性の高さです。
HonoはTypeScriptを活用しやすい設計になっています。
例えば、HonoではContextに独自の型を定義できます。
import { Hono } from "hono";
// Contextで使用する変数の型を定義
type AppEnv = {
Variables: {
userId: string;
};
};
// 型定義を適用したHonoアプリケーションを作成
const app = new Hono<AppEnv>();
// リクエストに共通する処理を定義
app.use("*", async (c, next) => {
// サンプル用のユーザーIDをContextに保存
c.set("userId", "user-123");
// 次のミドルウェアまたはルート処理へ進む
await next();
});
// Contextに保存したユーザーIDを返すAPI
app.get("/me", (c) => {
// ContextからユーザーIDを取得
const userId = c.get("userId");
// 取得したユーザーIDをJSON形式で返す
return c.json({
userId,
});
});
export default app;
このコードでは、userIdを文字列として定義しています。
type AppEnv = {
Variables: {
userId: string;
};
};
これにより、c.get("userId")で取得する値の型をTypeScriptに認識させられます。
例えば、存在しない変数名を指定した場合などに、型チェックによって問題を発見しやすくなります。
Contextとは?
ここで登場するContextは、Honoを理解するうえで重要な概念です。
Contextは、HTTPリクエストに関する情報を取得したり、レスポンスを生成したりするためのオブジェクトです。
例えば、以下のような操作ができます。
// URLパラメータを取得
const id = c.req.param("id");
// クエリパラメータを取得
const keyword = c.req.query("keyword");
// JSONレスポンスを返す
return c.json({ message: "OK" });
また、c.set()とc.get()を利用すると、ミドルウェアとルート処理の間で値を受け渡せます。
ただし、型を定義しただけで実行時の値が保証されるわけではありません。
外部から受け取ったデータについては、別途バリデーションが必要です。
TypeScriptの型を活用しながら、APIの実装を整理しやすいこともHonoの魅力だと感じました。
4. ミドルウェアを使って共通処理をまとめられる
個人的に、今回調べていて特に重要だと感じたのがミドルウェアです。
ミドルウェアとは、APIの本来の処理が実行される前後に、共通の処理を実行する仕組みです。
例えば、以下のような処理に利用できます。
- 認証
- ログ出力
- CORS設定
- エラー処理
- リクエストの検証
サンプル:簡単な認証ミドルウェア
import { Hono } from "hono";
import type { MiddlewareHandler } from "hono";
const app = new Hono();
// リクエストの認証情報を確認するミドルウェア
const authMiddleware: MiddlewareHandler = async (c, next) => {
// Authorizationヘッダーから認証情報を取得
const token = c.req.header("Authorization");
// 認証情報が一致しない場合は401エラーを返す
if (token !== "Bearer sample-token") {
return c.json(
{ error: "認証が必要です" },
401
);
}
// 認証成功時は次の処理へ進む
await next();
};
// /private以下のAPIに認証ミドルウェアを適用
app.use("/private/*", authMiddleware);
// 認証されたユーザーのみ利用できるAPI
app.get("/private/profile", (c) => {
// サンプルのユーザー情報を返す
return c.json({
name: "Taro",
message: "認証に成功しました",
});
});
export default app;
※このコードはミドルウェアの仕組みを理解するためのサンプルです。実際の認証では、JWTの署名や有効期限などを適切に検証する必要があります。
処理の流れ
APIリクエスト
↓
認証ミドルウェア
↓
認証情報を確認
│
├─ 認証失敗
│ ↓
│ 401エラー
│
└─ 認証成功
↓
await next()
↓
API本来の処理
↓
レスポンス
ここで重要なのが、以下のコードです。
await next();
next()を呼び出すことで、次のミドルウェアやルート処理へ進みます。
反対に、認証に失敗した場合は、401エラーを返して処理を終了します。
なぜミドルウェアが便利なのか?
例えば、認証が必要なAPIが10個存在するとします。
ミドルウェアを使用しなければ、それぞれのAPIに認証処理を書くことも考えられます。
しかし、同じ認証処理を何度も記述すると、コードが重複してしまいます。
そこで、認証処理をミドルウェアとして共通化します。
認証ミドルウェア
│
├─ ユーザー情報API
├─ 投稿一覧API
├─ 投稿詳細API
└─ 投稿作成API
これにより、認証処理を一か所にまとめられます。
また、認証方法を変更する場合も、共通処理を中心に修正できます。
なお、認証ミドルウェアを通過したからといって、すべてのデータにアクセスしてよいわけではありません。
例えば、投稿の編集や削除では、その投稿がログインユーザーの所有物であるかといった認可の確認も必要です。
共通処理をまとめ、各APIでは本来の処理に集中できることが、ミドルウェアの大きなメリットだと感じました。
5. ルーティングを整理しやすい
Honoでは、HTTPメソッドとURLを組み合わせてAPIを定義できます。
// ユーザー一覧を取得
app.get("/users", ...);
// ユーザーを登録
app.post("/users", ...);
// 指定したユーザーの情報を取得
app.get("/users/:id", ...);
それぞれの意味は以下のとおりです。
| メソッド | エンドポイント | 用途 |
|---|---|---|
| GET | /users |
ユーザー一覧を取得 |
| POST | /users |
ユーザーを登録 |
| GET | /users/:id |
指定したユーザーを取得 |
さらに、Honoではルートをファイルごとに分割できます。
例えば、以下のような構成です。
src/
├── app.ts
└── routes/
├── users.ts
└── posts.ts
users.ts
import { Hono } from "hono";
// ユーザー関連のルートを管理するHonoインスタンス
const users = new Hono();
// ユーザー一覧を取得するAPI
users.get("/", (c) => {
// サンプルのユーザー一覧を返す
return c.json([
{ id: 1, name: "Taro" },
{ id: 2, name: "Hanako" },
]);
});
// 指定されたユーザーIDの情報を取得するAPI
users.get("/:id", (c) => {
// URLからユーザーIDを取得
const id = c.req.param("id");
// サンプルのユーザー情報を返す
return c.json({
id,
name: "Sample User",
});
});
export default users;
app.ts
import { Hono } from "hono";
import users from "./routes/users.js";
// アプリケーション全体のHonoインスタンスを作成
const app = new Hono();
// ユーザー関連のルートを /api/users にまとめる
app.route("/api/users", users);
export default app;
これにより、以下のAPIを利用できます。
GET /api/users
GET /api/users/:id
このように、機能ごとにルートを分離できます。
APIの数が少ないうちは1つのファイルでも問題ありませんが、機能が増えると管理が難しくなります。
そのため、機能単位でルーティングを整理できることは、保守性の面でもメリットがあります。
6. 複数の実行環境に対応している
Honoの特徴として、複数のJavaScript実行環境に対応している点も挙げられます。
代表的な実行環境は以下のとおりです。
| 実行環境 | 概要 |
|---|---|
| Node.js | 一般的なJavaScript実行環境 |
| Bun | JavaScript・TypeScriptの実行環境 |
| Deno | JavaScript・TypeScriptの実行環境 |
| Cloudflare Workers | エッジ環境でコードを実行 |
| AWS Lambda | AWSのサーバーレス実行環境 |
なぜ複数の環境に対応できるのでしょうか。
その理由の一つが、Web標準のAPIを中心に設計されていることです。
Web標準のRequest・Response API
JavaScriptには、HTTPリクエストやレスポンスを扱うための標準的なAPIがあります。
例えば、以下のようなものです。
// 指定したURLへのHTTPリクエストを表すオブジェクトを作成
const request = new Request("https://example.com");
// JSON形式のレスポンスを作成
const response = new Response(
JSON.stringify({ message: "Hello" }),
{
headers: {
"Content-Type": "application/json",
},
}
);
Honoは、このようなWeb標準の仕組みを活用しています。
そのため、特定の実行環境に強く依存しないAPIを構築しやすくなっています。
Node.jsでの実行例
Node.jsでHonoを動かす場合は、アダプターを利用できます。
import { Hono } from "hono";
import { serve } from "@hono/node-server";
// Honoアプリケーションを作成
const app = new Hono();
// トップページにアクセスされた際の処理を定義
app.get("/", (c) => {
// テキスト形式でレスポンスを返す
return c.text("Hello Hono!");
});
// Node.js上でHTTPサーバーを起動
serve({
fetch: app.fetch,
port: 3000,
});
このコードでは、Node.js上でHTTPサーバーを起動しています。
Honoのルーティング処理はそのまま利用し、Node.jsとの接続部分をアダプターが担当します。
サーバーレス環境でも利用できる
HonoはAWS Lambdaなどのサーバーレス環境にも対応しています。
例えば、ローカル環境ではNode.jsを利用し、本番環境ではLambdaを利用するといった構成も考えられます。
ただし、コードを一切変更せずにすべての環境へ移行できるわけではありません。
実行環境によっては、アダプターやデプロイ設定、環境変数などの調整が必要です。
それでも、APIの主要な処理を共通化しやすい点は魅力だと感じました。
Expressとの違い
JavaScriptのバックエンドフレームワークとして有名なものに、Expressがあります。
ExpressもシンプルにAPIを開発できるフレームワークです。
では、Honoとは何が違うのでしょうか。
コードを比較してみる
まずは、ExpressでJSONを返すAPIを作成します。
Express
import express from "express";
// Expressアプリケーションを作成
const app = express();
// GET /hello の処理を定義
app.get("/hello", (req, res) => {
// JSON形式でレスポンスを返す
res.json({
message: "Hello Express!",
});
});
// ポート3000でHTTPサーバーを起動
app.listen(3000);
続いて、Honoの場合です。
Hono
import { Hono } from "hono";
import { serve } from "@hono/node-server";
// Honoアプリケーションを作成
const app = new Hono();
// GET /hello の処理を定義
app.get("/hello", (c) => {
// JSON形式でレスポンスを返す
return c.json({
message: "Hello Hono!",
});
});
// Node.js上でHTTPサーバーを起動
serve({
fetch: app.fetch,
port: 3000,
});
どちらもシンプルにAPIを実装できます。
大きな違いの一つは、リクエストとレスポンスの扱い方です。
Expressでは、主にreqとresを利用します。
// Expressではreqとresを利用する
(req, res) => {
res.json({ message: "Hello" });
}
一方、HonoではContextを利用します。
// HonoではContextを利用する
(c) => {
return c.json({ message: "Hello" });
}
この違いは、フレームワークの設計思想にも関係しています。
主な違い
| 比較項目 | Hono | Express |
|---|---|---|
| 主な特徴 | 軽量・Web標準重視 | シンプル・豊富な実績 |
| TypeScript | 型を活用しやすい設計 | 型定義を利用して対応 |
| 実行環境 | Node.js・Bun・Deno・エッジなど | 主にNode.js |
| ミドルウェア | 対応 | 対応 |
| ルーティング | 対応 | 対応 |
| エコシステム | 比較的新しい | 長年の実績がある |
| サーバーレス | 複数環境向けのアダプターを提供 | 構成次第で対応可能 |
Honoが向いているケース
- TypeScriptを中心に開発したい
- 軽量なREST APIを構築したい
- Web標準を意識した構成にしたい
- サーバーレスやエッジ環境を利用したい
- 複数の実行環境への対応を考えている
Expressが向いているケース
- 長年の実績があるフレームワークを利用したい
- 既存のExpress資産を活用したい
- 豊富なミドルウェアや事例を参考にしたい
- Node.jsを中心にバックエンドを構築したい
Expressにも長年培われた実績やエコシステムという大きな強みがあります。
そのため、必ずしもHonoの方が優れているというわけではありません。
開発するアプリケーションの要件や実行環境によって、適したフレームワークは変わると考えています。
Honoは何を担当するフレームワークなのか?
ここまでHonoの特徴を紹介してきましたが、もう一つ整理しておきたいことがあります。
それは、Honoが担当する処理と、担当しない処理の違いです。
例えば、以下のようなバックエンドを考えてみます。
フロントエンド
↓
HTTPリクエスト
↓
Hono
│
├─ ルーティング
├─ 認証ミドルウェア
├─ リクエストの検証
│
▼
アプリケーションの処理
│
├─ データベースへのアクセス
└─ 外部APIとの通信
│
▼
Hono
↓
JSONレスポンス
↓
フロントエンド
Honoは、HTTPリクエストを受け取り、必要な処理へ振り分け、レスポンスを返す役割を担っています。
一方、データベースそのものや、外部APIの処理をHonoが提供しているわけではありません。
例えば、以下のような役割分担になります。
| 機能 | 主な担当 |
|---|---|
| APIのルーティング | Hono |
| HTTPリクエスト・レスポンス | Hono |
| ミドルウェアの実行 | Hono |
| データベース | PostgreSQLなど |
| DBへのアクセス | ORM・DBドライバーなど |
| JWTの検証 | 認証ライブラリなど |
| 外部APIとの通信 | Fetch API・SDKなど |
| 画面表示 | React・Next.jsなど |
もちろん、Honoのミドルウェアやヘルパーを利用して、これらの処理を組み込むことはできます。
ただし、Hono自体がすべての機能を提供しているわけではありません。
今回調べてみて、Honoはバックエンドのさまざまな処理をHTTP APIとしてまとめるためのフレームワークだと理解できました。
Honoを採用する際に注意したいこと
ここまでHonoの魅力を紹介してきましたが、採用する際に注意したい点もあります。
1. 軽量だからといって必ず高速になるわけではない
Honoは軽量で高速なルーティングを特徴としています。
しかし、API全体の処理速度は、フレームワーク以外の要素にも影響されます。
例えば、以下のような処理です。
- データベースへのアクセス
- 外部APIとの通信
- 大量データの処理
- ネットワークの遅延
そのため、Honoを採用するだけで、すべてのAPIが高速になるわけではありません。
2. 必要な機能は別途組み合わせる場合がある
Honoは軽量なフレームワークですが、アプリケーションに必要なすべての機能が標準で揃っているわけではありません。
例えば、データベースへのアクセスにはORMやDBドライバーを利用することがあります。
また、認証やバリデーションについても、要件に応じて適切なライブラリを組み合わせる必要があります。
これは自由度が高いというメリットでもありますが、技術選定を自分で考える必要があるということでもあります。
3. 実行環境ごとの違いを理解する必要がある
Honoは複数の実行環境に対応していますが、それぞれの環境には違いがあります。
例えば、Node.jsで利用できる機能が、すべてのエッジ環境で利用できるとは限りません。
また、デプロイ方法や環境変数の設定も異なります。
そのため、Honoの対応環境だけでなく、利用するライブラリがその環境で動作するかも確認する必要があります。
AIを活用した開発だからこそ、技術の理解が重要だと感じた
今回、Honoについて改めて調べようと思った理由は、AIを活用した開発を進める中で、自分自身の理解が追いついていないと感じたためです。
AIを利用すると、APIの実装やエラーの修正などを効率的に進められます。
一方で、コードが動作していることと、そのコードを理解していることは別だと感じています。
例えば、Honoのミドルウェアを使って認証処理を実装できたとしても、
- なぜミドルウェアを利用するのか
-
await next()は何をしているのか - Contextはどのような役割を持っているのか
- なぜHonoを採用したのか
といった質問に答えられなければ、技術を十分に理解できているとは言えないと思います。
もちろん、すべての技術を完全に理解してから開発を始める必要はないと考えています。
実際に動かしながら理解を深めることも、学習方法の一つです。
ただ、AIによって開発を進めやすくなったからこそ、定期的に技術を振り返る時間を設けることが重要なのではないかと感じました。
まとめ
今回は、個人開発で利用しているHonoについて、特徴や魅力を改めて整理しました。
特に印象に残ったのは、以下の点です。
- 軽量でシンプルにAPIを実装できる
- 外部ライブラリへの依存が少なく、管理負担やセキュリティリスクを抑えやすい
- TypeScriptとの親和性が高い
- ミドルウェアによって共通処理を整理できる
- 機能単位でルーティングを分割できる
- 複数のJavaScript実行環境に対応している
中でも、個人的にはミドルウェアによる共通処理の管理と、依存ライブラリの少なさが特に印象に残りました。
また、Honoはバックエンドのすべてを担当するものではなく、HTTP APIを中心にさまざまな処理を組み合わせるためのフレームワークであることも理解できました。
今回の振り返りを通して、Honoを採用した理由について、以前よりも具体的に説明できるようになった気がします。
今後もAIを活用しながら開発を進めていく予定ですが、単にコードを動かすだけでなく、利用している技術の役割や仕組みを理解することも大切にしていきたいと思います。