Spaces:
Sleeping
Sleeping
| # API設計パターン | |
| ## 概要 | |
| このドキュメントでは、本プロジェクトにおけるAPI設計の標準パターンと実装ルールを定義します。 | |
| Gradio API をプロキシする `/gradio-proxy/check-url` の実装を参考例として、統一された設計アプローチを記載します。 | |
| ## アーキテクチャ概要 | |
| ``` | |
| フロントエンド (React) | |
| ↓ useCheckUrl() hook | |
| API Client (api-client/gradio-proxy/check-url.ts) | |
| ↓ hc<RpcType>().api.rpc['gradio-proxy']['check-url'] | |
| RPC Router (app/api/rpc/[...route]/route.ts) | |
| ↓ .route('/gradio-proxy', gradioProxyRoute) | |
| Server Route (server/routes/gradio-proxy.ts) | |
| ↓ .post('/check-url', zValidator(...)) | |
| Service Layer (services/gradio.ts) | |
| ↓ getCheckUrl() | |
| External API (Gradio) | |
| ``` | |
| ## ディレクトリ構造 | |
| ``` | |
| project-root/ | |
| ├── schema/ # Zodスキーマ定義 | |
| │ └── check-url.ts # check-url API用の型定義とバリデーション | |
| ├── server/routes/ # Honoルート定義 | |
| │ └── gradio-proxy.ts # Gradio APIプロキシのエンドポイント実装 | |
| ├── api-client/ # フロントエンド向けAPIクライアント | |
| │ └── check-url.ts # React Query hooks | |
| ├── app/api/rpc/[...route]/ # Next.js App Router統合 | |
| │ └── route.ts # RPCルート登録 | |
| └── services/ # ビジネスロジック層 | |
| └── gradio.ts # Gradio API呼び出し | |
| ``` | |
| ## 実装ステップ | |
| ### 1. Zodスキーマ定義 (`schema/`) | |
| **目的**: リクエスト・レスポンスの型安全性とバリデーション | |
| **実装例** (`schema/gradio-proxy/check-url.ts`): | |
| ```typescript | |
| import { z } from 'zod'; | |
| // リクエストスキーマ | |
| export const checkUrlRequestSchema = z | |
| .object({ | |
| ownUrl: z.string().min(1, '自社URLを入力してください。'), | |
| urlText: z.array(z.string()).min(3, '競合URLを3件以上入力してください。'), | |
| ownImage: z.string().nullable(), | |
| ownImageName: z.string().optional(), | |
| dummyMode: z.boolean().optional().default(false), | |
| userEmail: z.string().optional(), | |
| userIdentifier: z.string().min(1, 'ユーザー識別子が必要です。'), | |
| }) | |
| .refine((data) => data.ownUrl.trim() !== '', { | |
| message: '自社URLを入力してください。', | |
| path: ['ownUrl'], | |
| }); | |
| export type CheckUrlRequest = z.infer<typeof checkUrlRequestSchema>; | |
| // レスポンススキーマ | |
| export const checkUrlResponseSchema = z.union([ | |
| z.unknown(), // 外部APIの正常レスポンス | |
| z.object({ | |
| status: z.literal('error'), | |
| message: z.string(), | |
| }), | |
| ]); | |
| export type CheckUrlResponse = z.infer<typeof checkUrlResponseSchema>; | |
| ``` | |
| **ルール**: | |
| - リクエスト・レスポンス両方にZodスキーマを定義 | |
| - エクスポートする型は`z.infer`を使用 | |
| - バリデーションエラーメッセージは日本語で明確に | |
| - `dummyMode`など共通プロパティは`.optional().default()`で設定 | |
| ### 2. サーバーサイドルート (`server/routes/`) | |
| **目的**: エンドポイント実装とビジネスロジック呼び出し | |
| **実装例** (`server/routes/gradio-proxy.ts`): | |
| ```typescript | |
| import { checkUrlRequestSchema } from '@/schema/gradio-proxy/check-url'; | |
| import { getDummyData } from '@/server/utils/dummy-data'; | |
| import { getCheckUrl } from '@/services/gradio'; | |
| import { zValidator } from '@hono/zod-validator'; | |
| import { Hono } from 'hono'; | |
| export const gradioProxyRoute = new Hono() | |
| // POST /check-url | |
| .post('/check-url', zValidator('json', checkUrlRequestSchema), async (c) => { | |
| try { | |
| const request = c.req.valid('json'); | |
| console.log('[CHECK_URL API] Request metadata:', { | |
| ownUrl: request.ownUrl, | |
| urlCount: request.urlText.length, | |
| dummyMode: request.dummyMode, | |
| }); | |
| // ダミーモード分岐 | |
| if (request.dummyMode) { | |
| try { | |
| const dummyData = getDummyData('get_check_url.json'); | |
| return c.json(dummyData); | |
| } catch (error) { | |
| return c.json( | |
| { | |
| status: 'error', | |
| message: error instanceof Error ? error.message : 'ダミーデータの読み込みに失敗しました。', | |
| }, | |
| 404, | |
| ); | |
| } | |
| } | |
| // サービス層呼び出し | |
| const result = await getCheckUrl( | |
| { | |
| ownUrl: request.ownUrl, | |
| urlText: request.urlText, | |
| ownImage: request.ownImage, | |
| ownImageName: request.ownImageName, | |
| }, | |
| request.userEmail || null, | |
| request.userIdentifier, | |
| ); | |
| // エラーハンドリング | |
| if (result && typeof result === 'object' && 'status' in result && result.status === 'error') { | |
| return c.json(result, 400); | |
| } | |
| return c.json(result); | |
| } catch (error) { | |
| console.error('[CHECK_URL API] Error:', error); | |
| return c.json({ status: 'error', message: '不明なエラーが発生しました。' }, 500); | |
| } | |
| }) | |
| // GET /status - ステータス確認(複数パスの例) | |
| .get('/status', async (c) => { | |
| return c.json({ status: 'ok', service: 'gradio-proxy' }); | |
| }); | |
| ``` | |
| **ルール**: | |
| - `zValidator`でリクエストバリデーション | |
| - ダミーモードの処理はサーバーサイドで実装 | |
| - ログ出力は`[API_NAME]`プレフィックスを付ける | |
| - エラーレスポンスは`{ status: 'error', message: string }`形式 | |
| - 適切なHTTPステータスコード (400, 404, 408, 500) を返す | |
| - サービス層の関数を直接呼び出す | |
| - **重要**: `new Hono().get().post()`のようにメソッドチェーンで書く(型推論のため) | |
| **メソッドチェーンの重要性**: | |
| ```typescript | |
| // ✅ Good - rpcClientで型推論が正しく動作 | |
| export const checkUrlRoute = new Hono() | |
| .post('/', ...) | |
| .get('/status', ...) | |
| // ❌ Bad - rpcClientで型推論が動作しない | |
| export const checkUrlRoute = new Hono() | |
| checkUrlRoute.post('/', ...) | |
| checkUrlRoute.get('/status', ...) | |
| ``` | |
| メソッドチェーンを使わないと、`hc<RpcType>()`による型推論が機能せず、フロントエンドでのAPI呼び出し時に型安全性が失われます。 | |
| ### 3. RPCルート登録 (`app/api/rpc/[...route]/route.ts`) | |
| **目的**: Next.js App RouterへのHonoルート統合 | |
| **実装例**: | |
| ```typescript | |
| import { gradioProxyRoute } from '@/server/routes/gradio-proxy'; | |
| import { proposalRoute } from '@/server/routes/proposal'; | |
| import { screenshotRoute } from '@/server/routes/screenshot'; | |
| import { Hono } from 'hono'; | |
| import { handle } from 'hono/vercel'; | |
| const app = new Hono() | |
| .basePath('/api/rpc') | |
| .onError((err, c) => { | |
| console.error('Global error:', err); | |
| return c.json({ error: { message: err.message || 'Internal Server Error', code: 'SYSTEM_ERROR' } }, 500); | |
| }) | |
| .notFound((c) => { | |
| return c.json({ error: { message: 'Not Found', code: 'NOT_FOUND' } }, 404); | |
| }) | |
| .route('/screenshot', screenshotRoute) | |
| .route('/proposal', proposalRoute) | |
| .route('/gradio-proxy', gradioProxyRoute); | |
| export const GET = handle(app); | |
| export const POST = handle(app); | |
| export const PUT = handle(app); | |
| export const DELETE = handle(app); | |
| export const PATCH = handle(app); | |
| export type RpcType = typeof app; | |
| ``` | |
| **ルール**: | |
| - `.basePath('/api/rpc')`でベースパスを設定 | |
| - グローバルエラーハンドラと404ハンドラを定義 | |
| - 新しいルートは`.route()`で追加 | |
| - `RpcType`を必ずエクスポート(型推論のため) | |
| ### 4. APIクライアント (`api-client/`) | |
| **目的**: フロントエンドからのAPI呼び出しをhooks化 | |
| **実装パターン1: useMutation(単発実行向け)** (`api-client/gradio-proxy/check-url.ts`): | |
| ```typescript | |
| import type { RpcType } from '@/app/api/rpc/[...route]/route'; | |
| import { withTimeout } from '@/lib/utils'; | |
| import type { CheckUrlRequest, CheckUrlResponse } from '@/schema/gradio-proxy/check-url'; | |
| import { useMutation } from '@tanstack/react-query'; | |
| import { hc } from 'hono/client'; | |
| // RPCクライアント設定 | |
| const apiUrl = process.env.NEXT_PUBLIC_API_URL || ''; | |
| const rpcClient = hc<RpcType>(apiUrl).api.rpc; | |
| /** | |
| * check_url APIを呼び出すカスタムフック(mutation版) | |
| * @returns mutationオブジェクト(タイムアウト5分) | |
| */ | |
| export function useCheckUrl() { | |
| return useMutation({ | |
| mutationFn: async (request: CheckUrlRequest): Promise<CheckUrlResponse> => { | |
| console.log('[useCheckUrl] API呼び出し開始:', { | |
| ownUrl: request.ownUrl, | |
| urlCount: request.urlText.length, | |
| dummyMode: request.dummyMode, | |
| }); | |
| const apiCall = async (): Promise<CheckUrlResponse> => { | |
| const response = await rpcClient['gradio-proxy']['check-url'].$post({ | |
| json: request, | |
| }); | |
| if (!response.ok) { | |
| const errorData = (await response.json()) as { message?: string }; | |
| console.error('[useCheckUrl] API エラー:', errorData); | |
| throw new Error(errorData.message || 'check_url APIの呼び出しに失敗しました'); | |
| } | |
| const data = await response.json(); | |
| console.log('[useCheckUrl] API呼び出し成功'); | |
| return data as CheckUrlResponse; | |
| }; | |
| try { | |
| return await withTimeout(apiCall(), 5 * 60 * 1000); // 5分タイムアウト | |
| } catch (error) { | |
| console.error('[useCheckUrl] API呼び出しエラー:', error); | |
| throw error; | |
| } | |
| }, | |
| }); | |
| } | |
| ``` | |
| **実装パターン2: queryClient.fetchQuery(キャッシュ活用・複数API連携向け)** (`api-client/gradio-proxy/score-step3.ts`): | |
| ```typescript | |
| import { createQueryOptions } from '@/api-client/query-config'; | |
| import type { RpcType } from '@/app/api/rpc/[...route]/route'; | |
| import type { ScoreStep3Request, ScoreStep3Response } from '@/schema/gradio-proxy/score-step3'; | |
| import type { FetchQueryOptions } from '@tanstack/react-query'; | |
| import { useQueryClient } from '@tanstack/react-query'; | |
| import { hc } from 'hono/client'; | |
| import { useCallback } from 'react'; | |
| const apiUrl = process.env.NEXT_PUBLIC_API_URL || ''; | |
| const rpcClient = hc<RpcType>(apiUrl).api.rpc; | |
| /** | |
| * score_step3 APIを呼び出すカスタムフック | |
| * @returns キャッシュ対応のAPI呼び出し関数(タイムアウト5分、キャッシュ24時間) | |
| */ | |
| export function useScoreStep3() { | |
| const queryClient = useQueryClient(); | |
| return useCallback( | |
| async (request: ScoreStep3Request): Promise<ScoreStep3Response> => { | |
| return queryClient.fetchQuery( | |
| createQueryOptions<ScoreStep3Response>( | |
| { | |
| queryKey: ['score-step3', request], | |
| queryFn: async (): Promise<ScoreStep3Response> => { | |
| const response = await rpcClient['gradio-proxy']['score-step3'].$post({ | |
| json: request, | |
| }); | |
| if (!response.ok) { | |
| const errorData = (await response.json()) as { message?: string }; | |
| throw new Error(errorData.message || 'score_step3 APIの呼び出しに失敗しました'); | |
| } | |
| return await response.json(); | |
| }, | |
| }, | |
| 5 * 60 * 1000, // 5分タイムアウト | |
| ) as FetchQueryOptions<ScoreStep3Response>, | |
| ); | |
| }, | |
| [queryClient], | |
| ); | |
| } | |
| ``` | |
| **パターン選択のガイドライン**: | |
| | パターン | 用途 | 特徴 | | |
| | -------------------------- | ----------------------------- | ------------------------------------------ | | |
| | **useMutation** | 単発のAPI呼び出し | シンプル、mutation状態管理、キャッシュ不要 | | |
| | **queryClient.fetchQuery** | キャッシュが必要、複数API連携 | キャッシュ活用、React Query機能フル活用 | | |
| **共通ルール**: | |
| - `hc<RpcType>()`で型安全なクライアント生成 | |
| - `withTimeout`または`createQueryOptions`でタイムアウト管理 | |
| - エンドポイント名はケバブケース(`rpcClient['gradio-proxy']['check-url']`のようにネストする) | |
| - 詳細なログ出力(開始、成功、エラー) | |
| - エラーハンドリングを適切に実装 | |
| **パターン1(useMutation)の特徴**: | |
| - `useMutation`フックを直接使用 | |
| - `mutateAsync()`で呼び出し、mutation状態(isLoading, isError等)を取得可能 | |
| - `withTimeout`で直接タイムアウトを設定(5分が一般的) | |
| - キャッシュ不要な単発実行に最適 | |
| **パターン2(queryClient.fetchQuery)の特徴**: | |
| - `useCallback`でメモ化されたAPI呼び出し関数を返す | |
| - `createQueryOptions`でキャッシュ・タイムアウト設定を統一管理 | |
| - React Queryのキャッシュ機能を活用(同一リクエストの重複防止) | |
| - 複数のAPI呼び出しを連携させる場合に最適 | |
| **React Query設定** (`api-client/query-config.ts`): | |
| パターン2で使用する共通設定: | |
| ```typescript | |
| import { withTimeout } from '@/lib/utils'; | |
| import type { QueryFunctionContext, UseQueryOptions } from '@tanstack/react-query'; | |
| export const defaultQueryOptions: Partial<UseQueryOptions> = { | |
| // 自動再フェッチを無効化 | |
| refetchOnWindowFocus: false, | |
| refetchOnMount: false, | |
| // キャッシュ設定 | |
| staleTime: Infinity, // データは常に新鮮 | |
| gcTime: 24 * 60 * 60 * 1000, // 24時間キャッシュ保持 | |
| // リトライ設定(長時間APIのためリトライ無効) | |
| retry: 0, | |
| retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), | |
| // タイムアウト設定(デフォルト20分) | |
| meta: { | |
| timeout: 20 * 60 * 1000, | |
| }, | |
| }; | |
| /** | |
| * createQueryOptions - queryFnにタイムアウトを自動追加 | |
| * @param overrides 上書きしたいオプション | |
| * @param customTimeout カスタムタイムアウト時間(ミリ秒) | |
| */ | |
| export function createQueryOptions<T = unknown>(overrides?: Partial<UseQueryOptions<T>>, customTimeout?: number): Partial<UseQueryOptions<T>> { | |
| const timeout = customTimeout || (defaultQueryOptions.meta as { timeout: number })?.timeout || 10 * 60 * 1000; | |
| const wrappedOptions = { ...defaultQueryOptions, ...overrides }; | |
| if (wrappedOptions.queryFn && typeof wrappedOptions.queryFn === 'function') { | |
| const originalQueryFn = wrappedOptions.queryFn; | |
| wrappedOptions.queryFn = async (context: QueryFunctionContext) => { | |
| console.log(`[QUERY] API呼び出し開始 - タイムアウト: ${timeout / 1000}秒`); | |
| try { | |
| const promise = Promise.resolve(originalQueryFn(context)); | |
| const result = await withTimeout(promise, timeout); | |
| console.log(`[QUERY] API呼び出し成功`); | |
| return result; | |
| } catch (error) { | |
| console.error(`[QUERY] API呼び出し失敗:`, error); | |
| throw error; | |
| } | |
| }; | |
| } | |
| return wrappedOptions as Partial<UseQueryOptions<T>>; | |
| } | |
| ``` | |
| **キャッシュ戦略**: | |
| - `staleTime: Infinity`: 明示的に再フェッチしない限りキャッシュを使用 | |
| - `gcTime: 24時間`: メモリ効率とパフォーマンスのバランス | |
| - 同じリクエストパラメータなら自動的にキャッシュから返却 | |
| - タイムアウト処理が自動的に追加される(デフォルト20分) | |
| ### 5. フロントエンド統合 | |
| **パターン1(useMutation)の使用例**: | |
| ```typescript | |
| import { useCheckUrl } from '@/api-client/gradio-proxy/check-url'; | |
| export default function PreAnalysisInput() { | |
| const { dummyMode, userIdentifier } = useGlobalStore(); | |
| const { user } = useUserStore(); | |
| const { mutateAsync: checkUrl, isPending, isError } = useCheckUrl(); | |
| const onSubmit = async (data: FormData) => { | |
| try { | |
| const response = await checkUrl({ | |
| ownUrl: data.ownUrl, | |
| urlText: data.urlText, | |
| ownImage: data.ownImage || '', | |
| ownImageName: data.ownImageName, | |
| dummyMode, | |
| userEmail: user?.user?.email, | |
| userIdentifier, | |
| }); | |
| // エラーチェック | |
| if ((response as { status: string }).status === 'error') { | |
| setErrorMessage((response as { message: string }).message); | |
| return; | |
| } | |
| // 正常処理 | |
| // ... | |
| } catch (error) { | |
| console.error('API呼び出しエラー:', error); | |
| setErrorMessage('エラーが発生しました'); | |
| } | |
| }; | |
| return ( | |
| <div> | |
| <button onClick={() => onSubmit(formData)} disabled={isPending}> | |
| {isPending ? '送信中...' : '送信'} | |
| </button> | |
| {isError && <p>エラーが発生しました</p>} | |
| </div> | |
| ); | |
| } | |
| ``` | |
| **パターン2(queryClient.fetchQuery)の使用例**: | |
| ```typescript | |
| import { useScoreStep3 } from '@/api-client/gradio-proxy/score-step3'; | |
| export default function PreAnalysisCheck() { | |
| const scoreStep3Api = useScoreStep3(); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const onSubmit = async () => { | |
| setIsLoading(true); | |
| try { | |
| // キャッシュを活用して呼び出し | |
| const result = await scoreStep3Api({ | |
| ownUrl, | |
| urlText: competitionUrls, | |
| tempImages, | |
| fvInfos, | |
| cnInfo, | |
| dummyMode, | |
| userEmail, | |
| userIdentifier, | |
| }); | |
| // 結果を処理 | |
| // ... | |
| } catch (error) { | |
| console.error('API呼び出しエラー:', error); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| } | |
| ``` | |
| **使用ルール**: | |
| **パターン1(useMutation)**: | |
| - `mutateAsync`を使用してAPI呼び出し | |
| - `isPending`, `isError`等のmutation状態を活用 | |
| - ローディング状態は自動管理される | |
| - 単発の実行に適している | |
| **パターン2(queryClient.fetchQuery)**: | |
| - フックから返される関数を直接呼び出す | |
| - ローディング状態は手動管理(useState等) | |
| - 同じパラメータでの呼び出しは自動的にキャッシュから返却 | |
| - 複数のAPI呼び出しを連携させる場合に適している | |
| **共通ルール**: | |
| - `dummyMode`をリクエストに含める(サーバーサイドで分岐) | |
| - エラーハンドリングをtry-catchで実装 | |
| ## ダミーモードの扱い | |
| ### 基本方針 | |
| - **フロントエンド**: `dummyMode`プロパティをAPIリクエストに含める | |
| - **サーバーサイド**: `dummyMode`に応じて分岐処理 | |
| - `true`: `public/dummy/`からJSONファイルを読み込んで返す | |
| - `false`: 実際のサービス層(Gradio API等)を呼び出す | |
| ### ダミーデータ配置 | |
| ``` | |
| public/ | |
| └── dummy/ | |
| ├── get_check_url.json | |
| ├── get_proposal_fv.json | |
| └── ... | |
| ``` | |
| ### 実装パターン | |
| 共通のユーティリティ関数 `getDummyData` を使用します。 | |
| ```typescript | |
| import { getDummyData } from '@/server/utils/dummy-data'; | |
| if (request.dummyMode) { | |
| try { | |
| const dummyData = getDummyData('get_check_url.json'); | |
| console.log('[API_NAME] Dummy data loaded successfully'); | |
| return c.json(dummyData); | |
| } catch (error) { | |
| console.error('[API_NAME] Dummy data loading failed:', error); | |
| return c.json( | |
| { | |
| status: 'error', | |
| message: error instanceof Error ? error.message : 'ダミーデータの読み込みに失敗しました。', | |
| }, | |
| 404, | |
| ); | |
| } | |
| } | |
| ``` | |
| ### ユーティリティ関数 | |
| `server/utils/dummy-data.ts` に以下の関数を提供しています。 | |
| ```typescript | |
| /** | |
| * ダミーデータファイルを読み込む | |
| * @param fileName ファイル名(例: 'get_check_url.json') | |
| * @returns パースされたJSONデータ | |
| * @throws ファイルが存在しない場合やJSON解析に失敗した場合 | |
| */ | |
| export function getDummyData<T = unknown>(fileName: string): T; | |
| /** | |
| * ダミーデータファイルが存在するかチェック | |
| * @param fileName ファイル名 | |
| * @returns ファイルが存在する場合true | |
| */ | |
| export function hasDummyData(fileName: string): boolean; | |
| ``` | |
| ## エラーハンドリング | |
| ### エラーレスポンス形式 | |
| ```typescript | |
| { | |
| status: 'error', | |
| message: string // ユーザー向けエラーメッセージ(日本語) | |
| } | |
| ``` | |
| ### HTTPステータスコード | |
| - `400 Bad Request`: バリデーションエラー、API論理エラー | |
| - `404 Not Found`: リソースが見つからない | |
| - `408 Request Timeout`: タイムアウト | |
| - `500 Internal Server Error`: サーバー内部エラー | |
| ### エラーメッセージの日本語化 | |
| ```typescript | |
| if (error instanceof Error) { | |
| errorMessage = error.message; | |
| if (error.message.includes('タイムアウト') || error.message.includes('timeout')) { | |
| errorMessage = 'タイムアウトが発生しました。もう一度やり直してください。'; | |
| statusCode = 408; | |
| } | |
| } | |
| ``` | |
| ## ログ出力規約 | |
| ### フォーマット | |
| ``` | |
| [API_NAME] アクション: 詳細情報 | |
| ``` | |
| ### 例 | |
| ```typescript | |
| console.log('[CHECK_URL API] Request metadata:', { ... }); | |
| console.error('[CHECK_URL API] Error:', error); | |
| console.log('[useCheckUrl] API呼び出し開始:', { ... }); | |
| ``` | |
| ### 推奨ログポイント | |
| 1. **リクエスト受信時**: メタデータ(URLカウント、ダミーモード等) | |
| 2. **分岐処理時**: ダミーモード使用、フォールバック等 | |
| 3. **外部API呼び出し前後**: 開始と完了 | |
| 4. **エラー発生時**: エラー詳細とスタックトレース | |
| 5. **レスポンス返却時**: 成功メッセージ | |
| ## 型安全性の確保 | |
| ### Zodスキーマ → TypeScript型 | |
| ```typescript | |
| export const xxxRequestSchema = z.object({ ... }); | |
| export type XxxRequest = z.infer<typeof xxxRequestSchema>; | |
| ``` | |
| ### Hono RPC型推論 | |
| ```typescript | |
| // route.ts | |
| export type RpcType = typeof app; | |
| // api-client | |
| import type { RpcType } from '@/app/api/rpc/[...route]/route'; | |
| const rpcClient = hc<RpcType>(apiUrl).api.rpc; | |
| ``` | |
| これにより、エンドポイント名や型が自動補完され、タイプミスを防止できます。 | |
| ## パス指定ルール | |
| ### インポート | |
| - **禁止**: 相対パス (`../../../`) | |
| - **必須**: エイリアス (`@/`) | |
| ```typescript | |
| // ❌ Bad | |
| import { getCheckUrl } from '../../../services/gradio'; | |
| // ✅ Good | |
| import { getCheckUrl } from '@/services/gradio'; | |
| ``` | |
| ### Node.js組み込みモジュール | |
| ```typescript | |
| // Biomeルール準拠 | |
| import * as fs from 'node:fs'; | |
| import * as path from 'node:path'; | |
| ``` | |
| ## コーディング規約 | |
| ### import順序 | |
| 1. 外部ライブラリ | |
| 2. 内部モジュール(`@/`) | |
| 3. 相対パス(使用禁止) | |
| ### any型の禁止 | |
| ```typescript | |
| // ❌ Bad | |
| const data: any = await response.json(); | |
| // ✅ Good | |
| const data = (await response.json()) as CheckUrlResponse; | |
| ``` | |
| ### Prettierによる自動フォーマット | |
| ```bash | |
| npx prettier --write <編集したファイル> | |
| ``` | |
| ## チェックリスト | |
| 新しいAPIを追加する際は、以下を確認してください。 | |
| - [ ] Zodスキーマ定義 (`schema/`) | |
| - [ ] サーバーサイドルート実装 (`server/routes/`) | |
| - [ ] RPCルート登録 (`app/api/rpc/[...route]/route.ts`) | |
| - [ ] **APIクライアントパターンの選択**: | |
| - [ ] パターン1(useMutation): 単発実行、キャッシュ不要 | |
| - [ ] パターン2(queryClient.fetchQuery): キャッシュ活用、複数API連携 | |
| - [ ] APIクライアント作成 (`api-client/`) | |
| - [ ] フロントエンド統合 | |
| - [ ] ダミーモード対応 | |
| - [ ] エラーハンドリング | |
| - [ ] ログ出力 | |
| - [ ] Prettierフォーマット | |
| - [ ] 型安全性の確認 | |
| ## 参考実装 | |
| 完全な実装例として、以下のファイルを参照してください。 | |
| **パターン1(useMutation)の実装例**: | |
| - スキーマ: `schema/gradio-proxy/check-url.ts` | |
| - サーバールート: `server/routes/gradio-proxy.ts` (check-urlエンドポイント) | |
| - APIクライアント: `api-client/gradio-proxy/check-url.ts` | |
| - フロントエンド: `components/pages/index/pre-analysis-input.tsx` | |
| **パターン2(queryClient.fetchQuery)の実装例**: | |
| - スキーマ: `schema/gradio-proxy/score-step3.ts` | |
| - サーバールート: `server/routes/gradio-proxy.ts` (score-step3エンドポイント) | |
| - APIクライアント: `api-client/gradio-proxy/score-step3.ts` | |
| - 統合フック: `api-client/gradio-proxy/use-pre-analysis.ts` | |
| - フロントエンド: `components/pages/index/pre-analysis-check.tsx` | |
| **共通**: | |
| - RPCルート: `app/api/rpc/[...route]/route.ts` | |
| - Query設定: `api-client/query-config.ts` | |
| ## まとめ | |
| このパターンに従うことで、以下のメリットが得られます。 | |
| 1. **型安全性**: Zod + Hono RPCによるエンドツーエンドの型推論 | |
| 2. **保守性**: 統一された構造とログ出力 | |
| 3. **テスト性**: ダミーモードによる開発効率向上 | |
| 4. **拡張性**: 新しいAPIの追加が容易 | |
| 5. **エラー処理**: 一貫したエラーハンドリング | |
| 6. **柔軟性**: 用途に応じた2つの実装パターン | |
| - **useMutation**: シンプルな単発実行 | |
| - **queryClient.fetchQuery**: キャッシュ活用と複数API連携 | |