はじめに
こんにちは!!
最近は 超かぐや姫 を見て感動したり、
ゾンビランドサガのライブに行ったり、
胃腸炎にかかって苦しんだりしていた @hayatohanaoka です!!
安全な開発とクリーンアーキテクチャ寄りの設計 をアウトプットするため、TypeScript + Hono でサンプル API を作りました。
単体テスト・E2E(WireMock)・Docker / Kubernetes(Helm / Skaffold)・GitHub Actions CI まで一通り揃えています。
※ API の公開はしておらず、ローカルで動かす前提です。
Hono 自体の解説は割愛しますが、Hono がリクエストをどう処理しているかは過去記事で触れているので、興味のある方はこちらもどうぞ。
この記事では、設計で意識した点や技術スタック、テスト・CI まわりの構成を紹介します。
ソースコード
どんな API か
Qiita や Zenn など複数の記事サービスの API を裏で叩き、結果を集約して返す API です。クエリパラメータによる検索も実装しました。
また、外部 API の呼び出し先は環境変数(QIITA_URL / ZENN_URL)で切り替えられるため、本番では実際のサービスへ、テスト時は WireMock へ向けるといった使い分けができます。
エンドポイント一覧
| メソッド | パス | できること |
|---|---|---|
GET |
/v1/systems/ping |
ヘルスチェック(pong を返す) |
GET |
/api/v1/articles |
複数サービスの記事を集約して一覧取得 |
GET |
/api/v1/articles?q=… |
キーワードで記事を横断検索 |
レスポンスサンプル
/api/v1/systems/ping
$ curl -s localhost:13000/v1/systems/ping
pong
/api/v1/articles
※ body がかなり大きいので、一部抜粋
$ curl -s localhost:13000/api/v1/articles
{
"articles": [
{
"title": "【Java入門】完全修飾クラス名とは?長ったらしい名前の正体と使い方を分かりやすく解説🌱",
"url": "https://qiita.com/kamoi-kenichi/items/9dc9892f710c852616eb"
},
{
"title": "ANSYSメッシュ作成5つのポイント",
"url": "https://qiita.com/EdgeDevice/items/f8d71c929985c322a2fa"
},
(...中略...)
{
"title": "GitHub Copilot は自ら学ぶ: Copilot Memory 入門",
"url": "https://zenn.dev/microsoft/articles/50863342150992"
},
{
"title": "AIがコードを書くほど、要件定義は上に移動する――Spec・Context・Harness三層設計",
"url": "https://zenn.dev/gvatech_blog/articles/30f79910d111bb"
}
]
}
/api/v1/articles
※ body がかなり大きいので、一部抜粋
$ curl -s localhost:13000/api/v1/articles?q=claude
{
"articles": [
{
"title": "Emacs から Claude Code を使う helm の設定を書いた",
"url": "https://qiita.com/mori-dev@github/items/4ae373730eb4b71caae9"
},
{
"title": "Claude Codeで安全にバイブコーディングするためのセキュリティガイド【個人・チーム開発対応 / コピペで社内展開OK】",
"url": "https://qiita.com/kotaro_ai_lab/items/af25eb6608ff58893c74"
},
(...中略...)
{
"title": "AI駆動開発の実践(1)CLAUDE.mdとコンテキスト戦略 — AIに「現場」を伝える技術",
"url": "https://zenn.dev/miyan/articles/ai-driven-dev-claude-md-context"
},
{
"title": "🔍Claude CodeのSkill作成中に見かけた user-invocableを調べてみた",
"url": "https://zenn.dev/dely_jp/articles/76842757cce7b6"
}
]
}
意識したこと
TDD をしやすくする
単体テスト(app)と E2E(e2e)を、先に・繰り返し実行できるようにしています。外部の Qiita / Zenn 相当の応答は WireMock で固定し、Docker(CI)や Skaffold + Helm(ローカル Kubernetes でモックだけ立てる、など)と組み合わせて再現可能にしています。
CI でエラーに気づく仕組みを作る
main 向けの push / PR で、単体 → E2E(WireMock コンテナ + API 起動)→ コンテナイメージビルド(Skaffold)までを GitHub Actions で実行します。
E2E フィクスチャの分離
- 外部 API の応答は テストコードに直書きせず、WireMock 用のマッピング JSON を
e2e/fixtures/に置く。 - テストファイルのパスから fixture ディレクトリを自動解決する規約を採用。
e2e/tests/配下のパスをe2e/fixtures/へ読み替え、サービスごとのサブディレクトリ(qiita/・zenn/)を走査する(例:e2e/tests/api/v1/articles/get.test.ts→e2e/fixtures/api/v1/articles/get/{qiita,zenn}/*.json)。 - テスト側は
beforeEachでresetAllStubs()→setUpStubs(import.meta.filename)(e2e/src/setup-wiremock.ts)を呼ぶだけで、対応する fixture が WireMock Admin API に流し込まれる。 - 期待するレスポンスの大きな JSON をテスト本体から切り離し、ケース追加・差分レビューをしやすくする。
クリーンアーキテクチャ寄りの設計
ドメイン・ユースケース・ポート・ゲートウェイ・ドライバ(外部 HTTP)・REST(Hono)を分け、dependencies.ts で組み立てています。
技術スタック
| 領域 | 採用技術 |
|---|---|
| ランタイム | Node.js(Docker イメージは Node 24 Alpine ベース) |
| 言語 | TypeScript(module / moduleResolution: NodeNext) |
| Web フレームワーク | Hono |
| Node サーバー | @hono/node-server |
| 開発時実行 |
tsx(pnpm dev でウォッチ起動) |
| パッケージマネージャ | pnpm |
| 単体テスト |
Vitest(app、src/**/*.test.ts) |
| E2E |
Vitest(e2e) |
| 外部 API モック | WireMock(Docker 公式イメージ) |
| コンテナ | Docker(マルチステージビルド) |
| Kubernetes | Helm |
| ビルド・デプロイ連携 | Skaffold |
| CI | GitHub Actions |
ディレクトリ構成
| パス | 内容 |
|---|---|
app/ |
Hono API のソース、単体テスト、pnpm-lock.yaml
|
e2e/ |
E2E(Vitest)、WireMock 連携、fixtures/ にマッピング JSON |
environment/ |
Dockerfile、Skaffold、Helm、WireMock 用イメージ定義など |
CI(GitHub Actions)
main への push または pull request で、app/** に変更があるときにワークフローが走ります。
-
appで依存関係を入れ、Vitest で単体テスト - Docker で WireMock を 2 つ起動し、API をバックグラウンドで起動したうえで
e2eでpnpm vitest run -
environmentで Skaffold により Docker イメージをビルド
手動実行は workflow_dispatch からも可能です。
Docker / Kubernetes
| 項目 | 場所・内容 |
|---|---|
| Dockerfile |
environment/app/Dockerfile(Skaffold 上のビルドコンテキストは app/) |
| Skaffold |
environment/skaffold.yaml — ビルド後、Helm で hono-api をデプロイ可能 |
| WireMock 連携 |
with-mock プロファイルで WireMock 用イメージと Helm リリースを追加 |
| 本番向け調整 |
prd プロファイルでレプリカ数などを上書き |
クラスタ固有の設定やシークレットは、利用環境に合わせて Helm チャートを拡張してください。
Skaffold コマンドの使い分け
いずれも environment ディレクトリで実行します。ローカル Kubernetes と Skaffold が前提です。
| 目的 | コマンド | 補足 |
|---|---|---|
| イメージだけビルド | skaffold build |
主に CI と同様の用途。コンテナイメージのビルドのみで、アプリケーションは起動しません。 |
| E2E 用・モックだけ k8s | skaffold dev --port-forward |
API 本体は Kubernetes では動かさず、モックサーバー(WireMock)をローカルクラスタ上に立てます。API は別途 app で pnpm dev などとして起動し、ポートフォワードしたモックの URL を QIITA_URL / ZENN_URL に向けてから E2E を実行します。 |
| アプリだけ k8s | skaffold run --port-forward |
ローカル k8s で API だけデプロイするとき。モック用の依存サービスはクラスタ上では立てず、Helm のデフォルトどおり本番相当の URL へリクエストが飛びます。 |
skaffold dev で API を Kubernetes に載せずモックだけにしているのは、ローカル実行のほうが変更の反映が速いことに加え、本番用の Deployment に開発専用の volume 定義を混ぜたくないためです。
今後の伸び代
現状の CI では、E2E 用の WireMock を Docker コマンドで直接起動しています。ローカルでは Skaffold + Helm で管理できているのに対し、CI だけ別の手段になっている点が課題です。
改善の方向としては 2 通り考えています。
-
GitHub Actions 内で
skaffold dev --port-forwardを実行する — ローカルと同じ Helm チャートで WireMock を立てられるため、管理の一貫性が保てる - E2E 実行用のクラスタを別途用意する(クラウド or Actions 内部の k8s) — E2E リソースをそこにデプロイする形にすれば、CI ワークフロー自体はシンプルになる
いずれも「ローカルと CI で WireMock の立て方を揃える」ことがゴールです。
まとめ
TypeScript + Hono で、安全な開発とクリーンアーキテクチャ寄り設計を意識した API を作りました。
- TDD をしやすくするために、外部 API を WireMock でモックし、単体テストと E2E の両方をローカルで完結できるようにした
- CI でエラーに気づくために、GitHub Actions で単体テスト → E2E → イメージビルドまで自動実行するようにした
- E2E フィクスチャはテストコードから分離し、マッピング JSON として管理することで、ケース追加や差分レビューをしやすくした
- Docker / Kubernetes(Helm / Skaffold)を使い、ローカルでもコンテナ上でも同じ構成で動かせるようにした
上記の意識したことが少しでも伝わると嬉しいですし、業務ないしは個人開発をしようとしている方の参考になれたら嬉しいです!
Hono 自体は軽量ですし、フレームワーク特有の書き方のクセとかもないため、非常に書きやすかったです。
フレームワークの設計上、クリーンアーキテクチャ寄りの設計がし難いものもある中で、ここまで自由度高く書けるのはとても良いですね。
個人開発、マイクロサービスという観点に置いては、今後の技術選定の選択肢としても良いと思いました。