BluePeriod Docs
開発

バックエンド・アーキテクチャ

Honoフレームワークによるポータブルなバックエンド設計

16. バックエンド・アーキテクチャ (Hono & Node.js Runtime)

1. 概要

本プロジェクトのバックエンドは、Next.js App Router 内部で動作する Hono フレームワークを採用しています。これは単なる API 集約に留まらず、将来的なマルチプラットフォーム展開(Web/Desktop/Mobile)を見据えた「ポータブルなバックエンド」としての役割を担います。

1.1. 設計の柱

  • 集約 (Aggregation): Vercel の関数制限を回避するため、全 API を単一のエントリポイントに集約。
  • 安定性と互換性 (Node.js): 当初は Edge Runtime を採用していましたが、AI 連携ライブラリや DOM 操作を伴うツール(turndown等)との完全な互換性を確保するため、Node.js (Serverless Functions) ランタイムへ移行しました。これにより、標準的な Node.js API の利用と高い安定性を実現しています。
  • 疎結合 (Modular): 機能ごとにサブアプリケーションを分割し、AI アシスタントにとっても理解しやすい「小さなファイル」で構成。
  • 脳の分離 (Logic Extraction): ビジネスロジックを API ハンドラから切り離し、純粋な TypeScript 関数として src/lib/features に配置。

2. リクエスト・フロー

リクエストがクライアントから届き、レスポンスが返るまでの標準的なパスです。

  1. Entry Point: src/app/api/[[...route]]/route.ts (Next.js Adapter)
  2. Aggregation Layer: src/server/api/index.ts (Main Hono Instance)
  3. Global Middleware: Logger, CORS, Authentication
  4. Routing Layer: 各サブモジュール(infra.ts, stripe.ts等)への振り分け
  5. Controller Layer: リクエスト解析、バリデーション(Zod)
  6. Logic Layer: src/lib/features/... 内の純粋関数の実行(脳みそ
  7. Response: JSON または Streaming による応答

3. モジュール構造

バックエンド・コードは src/server/api/ 内でセマンティックに分割されています。

3.1. エントリポイント

  • src/app/api/[[...route]]/route.ts
    • Next.js の Catch-all Routes を利用したアダプター。
    • Hono のインスタンスをデプロイし、一貫性のために Node.js ランタイムを指定 (export const runtime = 'nodejs')。

3.2. サブアプリケーション (src/server/api/)

ファイル役割・ドメイン主な機能
index.tsMain Aggregatorサブアプリのマウント、グローバルミドルウェアの設定。
infra.tsInfrastructure疎通確認、システム時刻、プロキシ処理。
auth.tsAuthenticationSupabase 認証コールバック、セッション管理。
stripe.tsStripe Billing決済セッション作成、Webhook 受信、同期処理。
projects.tsProjects文芸プロジェクトの公開ステータス、メタデータ取得・編集、非公開化。
library.tsLibraryユーザーライブラリへの本棚アイテム追加、削除、並び替え。
publish.tsPublishing作品の公開ウィザード、画像・コンテンツのアップロード・DB登録。
ai.tsAI Intelligenceプロット構造化、改修、査読、メタデータ生成、多言語翻訳。
chat.tsChat & Agent通常チャットおよびエージェント実行、ストリーミング応答、ツール実行。
tools.tsToolsWeb 検索, URL 展開、AI 編集 (Adit) 等のエージェント用ツールプロキシ。
seo.tsSEO動的な sitemap.xml 生成およびサイトマップ用フィールドの取得。

4. ランタイムの選定と最適化 (Node.js移行の背景)

当初は Vercel Edge Runtime での動作を目指していましたが、以下の課題を解決するため Node.js ランタイムへ統合されました。

4.1. 移行の理由

  • ライブラリ互換性: エージェント用ツールで使用する turndown がブラウザの document オブジェクトを期待する(Node.js Shim で対応可能だが Edge では困難)など、ライブラリ制約があったため。
  • 標準 API の不足: CompressionStream やストリーム処理において、Edge Runtime 特有の制限による予期せぬエラー(not supported in the Edge Runtime)を回避するため。
  • AI プロバイダー SDK の安定性: SDK 内部で Node.js 固有の依存関係が含まれる場合があり、それらとの親和性を高めるため。

4.2. 実装上の注意

  • Runtime 明示: 各 Hono ルートファイル先頭で export const runtime = 'nodejs' を宣言し、Next.js に正しい解析を促します。
  • Auth: セキュリティ上の理由から supabase.auth.getSession() ではなく supabase.auth.getUser() を使用してセッションを検証します。この挙動は Node.js ランタイムでも同様です。

5. ロジック抽出戦略 (Logic Extraction)

API ハンドラ内には、外部サービスへの直接の依存や複雑なアルゴリズムを記述しません。

// ❌ 悪い例: ハンドラ内で詳細な Stripe の処理を書く
app.post('/checkout', (c) => {
  const stripe = new Stripe(...);
  // ... 50行のロジック ...
});

// ✅ 良い例: ロジックは lib に逃がす
import { createCheckoutSession } from '@/lib/features/stripe/service';

app.post('/checkout', async (c) => {
  const user = c.get('user');
  const session = await createCheckoutSession(user.id); // lib の純粋関数を呼ぶだけ
  return c.json(session);
});

この分離により、Tauri(デスクトップ版)などで「API を介さず直接ロジックを呼び出す」ことが容易になります。


5. ミドルウェアと認証

5.1. グローバル認証ミドルウェア (Optional Auth)

Hono の app.use('*', ...) を用い、ブラウザのクッキーから Supabase セッションの有無を常に確認します。セッションがある場合は c.set('user', user) でコンテキストにユーザー情報を保存しますが、ミドルウェアレベルでリクエストを遮断(401)することはありません。

認証(ログイン)が必須かどうかは、各ハンドラまたは Service レイヤーで判断されます:

  • AI 対話・ツール実行: 原則としてログイン不要(API キーがあれば動作)。
  • プライベートデータ操作: Supabase DB への書き込み等を伴う場合、ハンドラ内で c.get('user') を確認し、未ログインなら 401 を返却。
  • 公開コンテンツ参照: 公開作品の取得などは未ログインでも可能。

これにより、IndexedDB をベースとしたクライアント完結の動作(Local-First)を妨げることなく、クラウド連携機能のみを安全に保護しています。


6. マルチプラットフォーム展開への橋渡し

このバックエンド設計は、以下のプラットフォーム差分を吸収します。

  • Web: Hono は Vercel Edge Runtime 上で実行。
  • Desktop (Local):
    • 認証不要な AI ロジックなどは、クライアントサイドから src/lib/features を直接 import して実行。
    • 決済や公開などのクラウド必須機能のみ、Tauri から Web 上の Hono API を fetch する。
  • Security: すべての API リクエストにおいて、Hono がバリデーションおよび認証のゲートキーパーとして機能。

7. バリデーションとエラーハンドリング

API の品質とフロントエンドとの連携を強化するため、src/server/api/common.ts にて以下の仕組みを集約しています。

7.1. zValidator による型安全性の確保

リクエストの検証には zValidator を使用します。独自の validationHook を通すことで、バリデーション失敗時に詳細なエラー情報(どの項目が原因か等)を自動的に返却します。

infra.post('/proxy', zValidator('json', proxySchema, validationHook), async (c) => {
    // バリデーションに成功した場合のみ、型安全にデータを受け取れる
    const { url } = c.req.valid('json');
    // ...
});

7.2. エラーレスポンスの統一

errorResponse オブジェクトにより、全ての API で共通のエラーフォーマットを提供します。

  • unauthorized: 401 Unauthorized。認証漏れを指摘。
  • notFound: 404 Not Found。リソース不在を通知。
  • internalError: 500 Internal Server Error。内部で確実にログ (console.error) を出力した上で、エラー内容を安全にフロントエンドに伝えます。

これにより、API ごとにログの出力形式がバラバラになったり、エラー時のレスポンス構造が異なるといった問題を回避しています。


関連ドキュメント:

  • 01_architecture.md
  • 05_api_specification.md
  • 13_hybrid_architecture.md
  • 15_stripe_integration_guide.md
  • 06_development_guidelines.md

On this page