受託で小さめのバックエンドAPIを引き受けるたび、同じ作業を最初からやり直している感覚がある。Expressでアプリの初期化を書き、CORSとロガーのミドルウェアを並べ、リクエストボディのバリデーションを手書きで足し、最後にデプロイ構成を整える。中身の業務ロジックは案件ごとに違うのに、その周りを固める「土台」のコードがほぼ毎回コピペからの微修正になる。さらに厄介なのが型だ。サーバが返すレスポンスの形を、フロント側でもう一度手で定義し直す。APIを1つ直すたびに、サーバとフロントの両方で型を書き換える二重管理が常態化していた。
この消耗の正体は、フレームワーク選定が「とりあえずExpress」で止まっていたことにある。Expressは枯れていて情報も多いが、TypeScriptとの相性、エッジ/サーバーレスへのデプロイ、フロントとの型共有という、いま受託で求められる3点に対しては素直に答えてくれない。そこで近年、受託の小〜中規模APIの土台としてHonoを採用する判断が増えている。本記事では、Honoでバックエンドを組むときの実務上の勘所を、ディレクトリ構成・バリデーション・型共有・デプロイ先選定まで、実際の案件での判断とあわせて整理する。
なぜいま受託APIにHonoなのか
Honoは、Webの標準API(Fetch API)の上に作られた軽量なWebフレームワークだ。最大の特徴は、同じコードがランタイムを問わず動くこと。Node.js・Bun・Deno・Cloudflare Workers・AWS Lambda・Vercel・Netlifyなど、Fetch APIを話す環境ならほぼ書き換えなしで載る。受託では案件によってインフラの前提が変わるので、「コードを書き直さずにデプロイ先だけ差し替えられる」という性質が効いてくる。
数字の裏付けもある。npmの週間ダウンロード数は2026年1月時点で900万を超え、1年前の60万前後から一気に伸びた。プロダクションでの採用例もCloudflare(D1やWorkers KVの内部API)、Clerk、Unkey、OpenStatus、cdnjsなど実在のサービスが並ぶ。「新しいから不安」という段階はすでに過ぎていて、エッジ/サーバーレス前提の土台としては一級の選択肢になっている。バージョンは執筆時点(2026年6月)でv4系が安定版だ。
受託の観点で整理すると、Honoが効くのは次のような案件だ。
| 案件の性質 | Honoが向く理由 |
|---|---|
| 小〜中規模のREST API・BFF | 土台が薄く、立ち上げから業務ロジックまでが速い |
| エッジ/サーバーレス前提 | Workers・Lambdaへ書き換えなしで載る |
| フロントを同一チームで持つ | RPCでサーバ・フロント間の型を共有でき二重管理が消える |
逆に、すでにNestJSのような重量級フレームワークで大規模なドメイン層を組んでいる案件や、特定のNode専用ライブラリに深く依存している既存システムの保守では、無理に乗り換える理由は薄い。Honoは「土台を薄く速く」が刺さる領域のための道具だと割り切ったほうがいい。
ディレクトリ構成は「レイヤー」より「機能」で割る
Honoは構成を強制しないフレームワークなので、ディレクトリの切り方は自分で決める必要がある。受託で複数人が触り、後から保守も引き受けるケースでは、ここを最初に決めておかないと後で破綻する。
トレンドになった「Honoでバックエンドを作るときの個人的ベストプラクティス」(Zenn、2026年6月公開)でも整理されているのが、レイヤー単位ではなく機能(feature)単位で割る考え方だ。controllers/ services/ repositories/ のように技術レイヤーで横に割ると、1つの機能を直すたびに複数のディレクトリを行き来することになる。代わりに src/features/users/ src/features/posts/ のように機能でまとめ、その中にルート定義・バリデーションスキーマ・ロジックを同居させると、変更が一箇所に閉じる。
src/
features/
users/
route.ts # ルート定義(Honoインスタンス)
schema.ts # zod/valibotスキーマ
handler.ts # 業務ロジック
posts/
route.ts
schema.ts
handler.ts
middleware/ # 横断的ミドルウェア(認証・ロガー等)
lib/ # DB接続・外部クライアント
app.ts # 各featureのルートを束ねるルート集約
ルートの束ね方は、Honoのインスタンスを機能ごとに作ってroute()で合流させる。このとき後述するRPCの型推論を効かせるため、メソッドはチェーンでつなぐのが重要だ。
// src/features/users/route.ts
import { Hono } from 'hono'
const users = new Hono()
.get('/', (c) => c.json({ users: [] }))
.get('/:id', (c) => c.json({ id: c.req.param('id') }))
export default users
// src/app.ts
import { Hono } from 'hono'
import users from './features/users/route'
import posts from './features/posts/route'
const app = new Hono()
.route('/users', users)
.route('/posts', posts)
export default app
export type AppType = typeof app
最後にAppTypeをエクスポートしているのが後で効いてくる。これがフロントとの型共有の起点になる。
バリデーションはStandard Schema対応で書き、validatorを噛ませる
受託APIで事故が起きやすいのは、入力のバリデーションが甘い箇所だ。Honoは検証を組み込みのvalidatorミドルウェアで受け持つ。スキーマライブラリはzod・valibot・TypeBox・ArkTypeなど、Standard Schema(バリデーションライブラリ共通の仕様)に対応したものを選べる。
定番は@hono/zod-validatorだ。zodで定義したスキーマをzValidatorに渡し、検証対象(json / query / param など)を指定する。検証を通ったデータは型付きでc.req.valid()から取り出せる。
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
const schema = z.object({
name: z.string().min(1),
email: z.string().email(),
})
const users = new Hono().post(
'/',
zValidator('json', schema),
(c) => {
const data = c.req.valid('json') // ここで name/email は型付き
return c.json({ created: data }, 201)
}
)
バンドルサイズを絞りたい、特にエッジへ載せる案件では、zodより軽いvalibotを@hono/standard-validator経由で使う選択もある。APIの書き味はほぼ変わらないので、「Node常駐ならzod、Workersでサイズを詰めたいならvalibot」くらいの基準で割り切ってよい。
検証に失敗したときの返し方は揃えておきたい。zValidatorは第3引数にフックを取れるので、エラー時のレスポンス形式を統一できる。
const users = new Hono().post(
'/',
zValidator('json', schema, (result, c) => {
if (!result.success) {
return c.json(
{ error: 'ValidationError', issues: result.error.issues },
400
)
}
}),
(c) => c.json({ ok: true })
)
なお、スキーマ駆動でOpenAPIドキュメントまで自動生成したい場合はhono-openapiを組み合わせる手もある。OpenAPIを起点に型を配る設計の是非については、OpenAPIからoRPCへ型共有の記事で別アプローチと比較しているので、ドキュメント生成まで含めて設計を決めるときの参考にしてほしい。
RPCでフロントとの型の二重管理をなくす
冒頭で挙げた「サーバとフロントで型を二重に書く」問題に、Honoは独自の答えを持っている。RPC機能だ。サーバ側で先ほどエクスポートしたAppTypeを、フロント側のクライアントhcにジェネリクスとして渡すと、エンドポイントのパス・リクエスト・レスポンスの型がフロントに伝わる。コード生成(codegen)は要らない。
// フロント側
import { hc } from 'hono/client'
import type { AppType } from '../../server/src/app'
const client = hc<AppType>('https://api.example.com')
// パス補完が効き、resの型もサーバ定義から推論される
const res = await client.users.$post({
json: { name: 'Taro', email: '[email protected]' },
})
const data = await res.json()
サーバのスキーマを直すと、フロント側の該当箇所が即座に型エラー(赤線)になる。「APIを変えたのにフロントの修正を忘れて本番で壊れる」事故が、ビルド時に潰れる。これが受託で効く。
ただし型推論を効かせるには注意点が2つある。1つは、前述のとおりルートのメソッドを必ずチェーンでつなぐこと。途中で変数に代入して分断すると、型がhcまで届かない。もう1つは、モノレポでサーバとフロントを同居させる場合、両方のtsconfig.jsonのcompilerOptionsで"strict": trueを有効にしておくこと。これを外すとRPCの型がうまく解決されない。
RPCはサーバ・フロントを同一チームで持つ受託で特に強いが、相手が外部のフロントチームだったりOpenAPIでの連携を求められる案件では別の手が要る。型共有のアプローチ選定そのものはOpenAPIからoRPCへ型共有の記事で整理しているので、案件の体制にあわせて選んでほしい。
ミドルウェア・エラーハンドリング・テストの作法
横断的な処理はミドルウェアに寄せる。認証・ロギング・CORSなどはHono組み込みやサードパーティのミドルウェアで足りることが多く、独自処理はcreateMiddlewareで型付きに書ける。
import { createMiddleware } from 'hono/factory'
const auth = createMiddleware<{ Variables: { userId: string } }>(
async (c, next) => {
const token = c.req.header('Authorization')
if (!token) return c.json({ error: 'Unauthorized' }, 401)
c.set('userId', 'resolved-user-id')
await next()
}
)
エラーハンドリングはapp.onErrorで集約し、想定内のエラーはHTTPExceptionを投げて捕まえる。これでハンドラ内にtry/catchを散らかさず、レスポンス形式を一箇所で揃えられる。
import { HTTPException } from 'hono/http-exception'
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status)
}
console.error(err)
return c.json({ error: 'InternalServerError' }, 500)
})
テストはHonoの大きな利点だ。アプリはapp.request()で実HTTPサーバを立てずに直接叩ける。Vitestなどと組み合わせれば、軽量なリクエスト単位のテストが速く回る。
import { describe, it, expect } from 'vitest'
import app from '../src/app'
describe('users API', () => {
it('returns 400 on invalid body', async () => {
const res = await app.request('/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '' }),
})
expect(res.status).toBe(400)
})
})
受託では納品後の保守も引き受けることが多いので、この「サーバを立てずに振る舞いをテストできる」性質は、回帰を抑える土台として地味に効いてくる。
デプロイ先は「運用を誰が持つか」で決める
最後がデプロイ先の選定だ。Honoは同じコードがどこでも動くぶん、選択肢が多くて逆に迷う。受託では性能の最大値より、「運用を誰が・どこまで持つか」で決めるのが現実的だ。
| デプロイ先 | 向く案件 | 注意点 |
|---|---|---|
| Cloudflare Workers | エッジで低レイテンシ、スパイクのある公開API | Node固有API・一部ライブラリが使えない、実行時間・サイズ制約 |
| AWS Lambda | 既存がAWS、VPC内リソースに繋ぐ業務系 | コールドスタート、構成管理(IaC)の整備が要る |
| Node.js(常駐) | 既存のNode資産・常駐前提のバッチ併設 | サーバ運用・スケールを自前で持つ |
| Bun | 自社/小規模で速度を取りたい | 採用実績・運用ノウハウがまだ薄い案件もある |
Workersはエッジで速くスケールも任せられる反面、Node専用のAPIや一部ライブラリが動かず、実行時間やバンドルサイズの制約がある。既存資産がAWSにあり、VPC内のDBなどに繋ぐ業務システムなら素直にLambdaが収まりがいい。Lambda上でNode向けに書いたアプリを動かす移行手法はAWS Lambda Web Adapterサーバーレス移行の記事で具体的に扱っているので、既存をサーバーレスに寄せる検討と一緒に読むと判断しやすい。CloudflareとAWSのどちらに土台を置くかという、より上流のインフラ選定はCloudflareとAWSのインフラ選定の記事で整理している。
弊社が支援したある案件(小売・EC系のSaaS、社内3名規模のチーム、伏せる)では、商品データを配信する読み取り中心のAPIをHono+Cloudflare Workersで組み直した。採用理由は、世界各地のエンドユーザに対しエッジで低レイテンシに返したかったことと、フロント(Reactのフロントエンド)と同一チームだったためRPCで型共有して二重管理を消したかったことの2点。結果として、ExpressベースだったころにフロントとAPIの型ずれで月に数件起きていた本番不具合が、型エラーがビルドで止まるようになってからは目に見えて減った。一方で、決済まわりのNode専用SDKに依存する書き込み系APIは、無理にWorkersへ載せず別途Lambda側に置いた。「全部を1つのランタイムに寄せない」割り切りが、Honoのマルチランタイム性を活かす実務上のコツだった。
まず手を動かすなら
Honoは「薄くて速い土台」を素直に提供してくれるぶん、構成・バリデーション・型共有・デプロイ先という設計判断を自分で握る必要がある。逆に言えば、その判断さえ最初に固めれば、案件ごとに土台を作り直す消耗からは抜けられる。
手を動かすなら、まずは既存の小さなAPIを1本だけHonoに移し、サーバ側でAppTypeをエクスポートしてフロントをhcクライアントに差し替えてみてほしい。型の二重管理が消える感覚が、いちばん効果を実感しやすい入口になる。そのうえで、デプロイ先をWorkersにするかLambdaにするかは、運用を誰が持つかという案件固有の事情で決めればいい。
受託のバックエンドをHonoで設計し直したい、エッジ/サーバーレス前提で既存APIを移したいといったご相談は、グリームハブのお問い合わせからお寄せください。既存の構成とインフラ前提を拝見したうえで、土台の設計とデプロイ先選定をご一緒に詰めます。