HonoとCloudflareのサービスでHeadless CMSを作った

# HonoとCloudflareのサービスでHeadless CMSを作った
作ったと言ってますが、まだまだ開発途中です。ただ、メインの機能は大体実装しました！

## TL;DR
- honoとcloudflare workers, pagesの相性は最強レベル
- honoを利用することで、フルスタックアプリケーションのバックエンド部分に変更をほとんど加えないまま、フロントエンド部分を差し替えられる
- honoとvitest、@cloudflare/vitest-pool-workersの組み合わせのDXは素晴らしい
- 実装方法によっては意外とレスポンスが遅い
## 経緯
ある日Xで

[HonoHub](https://honohub.dev/)

という、HonoをベースとしたHeadless CMSのOSSを見つけました。
HonoとVite, Drizzle, Zodベースでありすごくモダンで触ってみたい！と思い早速試してみました。
ただ、ドキュメントではDBがneonというpostgresベースのDBaaSを紹介されており、自分はここでCloudflare D1を使いたかったため、断念しました。
https://honohub.dev/quickstart#setting-up-hono-hub 

ないなら自分で試してみようと思い、1から作ってみることにしました。

## github

[edge-cms](https://github.com/Stead08/edge-cms/tree/main)

## 機能
- 管理画面上でのデータスキーマ作成
- 記事CRUD機能
- ユーザー認証機能（認可処理は未実装)

## 技術スタック
Hono, Vite, Zodベースは変更せず、Cloudflareのサービスのみで運用できるように技術選定を行いました。

- Cloudflare群
  - Cloudflare workers (with service bindings)
  - Cloudflare D1 (Sqlite)
  - Cloudflare R2
  - Cloudflare Pages
- 言語
  - Typescript
  - SQL
- フレームワーク
  - Hono
  - Vite
  - React Router v7
- ライブラリ
  - テストライブラリ
    - vitest
  - バリデーションライブラリ
    - zod
  - モノレポツール
    - turbo
  - リンタ&フォーマッタ
    - biome
  - DB
    - sqlc
  - UI
    - shadcn/ui


Web API構築はHonoとzodを利用しており、apiのコアの依存パッケージはHonoとzodのみです。
管理画面はRemixで構築しました。開発途中でReact Router v7が安定版リリースしましたので、React Router v7に乗り換えました。UIは主にshadcn/uiを利用しました。

![スクリーンショット 2024-12-08 16.04.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687149/6e75dfea-f0d6-02d6-6da4-fbc2ea4d3e41.png)



## ディレクトリ構造
TurboRepoを利用し、モノレポを採用しています。
```
.
├── README.md
├── apps
│   ├── admin
│   ├── docs
│   └── sandbox
├── biome.json
├── lefthook.yml
├── package.json
├── packages
│   ├── auth
│   └── core
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── renovate.json
└── turbo.json
```

## ポイント
ここまで、概要をざっくり説明してきましたが、今回Honoを採用してみてすごく良かった点を紹介します。

### テストが異常に簡単
Hono公式にもドキュメントがあり、セットアップが簡単にできました。

https://hono.dev/examples/cloudflare-vitest

@cloudflare/vitest-pool-workers/configを利用します。
Cloudflare D1等Cloudflareサービスをモックするために以下のようにvitest.config.tsを設定します。
```typescript
import path from "node:path";
import {
	defineWorkersProject,
	readD1Migrations,
} from "@cloudflare/vitest-pool-workers/config";

export default defineWorkersProject(async () => {
	// Read all migrations in the `migrations` directory
	const migrationsPath = path.join(__dirname, "db");
	const migrations = await readD1Migrations(migrationsPath);

	return {
		test: {
			globals: true,
			setupFiles: ["./test/apply-migrations.ts"],
			poolOptions: {
				workers: {
					singleWorker: true,
					wrangler: {
						configPath: "./wrangler-test.toml",
						environment: "production",
					},
					miniflare: {
						// Add a test-only binding for migrations, so we can apply them in a
						// setup file
						bindings: { TEST_MIGRATIONS: migrations },
					},
				},
			},
		},
	};
});
```
テスト時に使用するwrangler-test.tomlを作成します。
```
name = "core"
main = "./src/index.ts"
compatibility_date = "2024-01-01"
compatibility_flags = ["nodejs_compat"]


[env.production.vars]
AUTH_SECRET = "test"
JWT_SECRET = "test"

[[env.production.kv_namespaces]]
binding = "KV"
id = "00000000000000000000000000000000"

[[env.production.r2_buckets]]
binding = "R2"
bucket_name = "r2-bucket"


[[env.production.d1_databases]]
binding = "DB"
database_name = "database"
database_id = "00000000-0000-0000-0000-000000000000"
```

ここまで行うと、vitestでwatchオプションを使用しながら、快適に実装を進めることができます！

### フロントエンドとの密な連携を実現しながら、フロントエンドのフレームワークに依存しない
honoのRPCを使用することにより、型安全にAPIにアクセスできます。
しかもhonoはWeb標準に則って実装されており、Web標準のRequest, ResponseをIOにとるサーバーサイドフレームワークであれば簡単に結合できます。
つまり、Next,Remix,Nuxt等のフルスタックフレームワークのサーバーサイド部分でHonoを利用できるということです。

Honoでサーバーサイドを実装することで、型安全なRPCを実現しながらフレームワークを柔軟に差し替えることができ、これは大きなメリットだなと思いました。

### 原因は調査中だが、意外とレスポンスが速くなかった。
ログイン機能等もhonoで実装したのですが、意外とレスポンスが速くなかったです。これはD1とのやりとりも必要かつキャッシュも使えない？からと思われます。とは言っても許容できないような遅さではないので、時間があったら調査しようと思います。

## まとめ
HonoとVite、そしてCloudflareのサービスは非常に相性が良く、特に小規模から中規模のサービス開発において大きな利点があると考えています。
これらを組み合わせることで：
- 開発速度の大幅な向上が見込める
- 各機能間の連携を密に保ちながらも、適度な独立性を維持できるため保守性が高い
- Cloudflareのサービスを活用することでコスト面でも優位性がある
以上の特徴から、実務での採用を検討する価値が高いスタックだと考えています。