検索/TypeScriptのフルスタックアプリを、サーバーとクライアント共通のAPIスキーマで作る

TypeScriptのフルスタックアプリを、サーバーとクライアント共通のAPIスキーマで作る

フルスタックのTypeScriptアプリを作る・直す時に使う。APIのリクエストとレスポンスをルートとクライアントのフックで共通のスキーマ1つにまとめ、入力を読む前に認証し、依存関係・キャッシュ・コンポーネントの見た目をそろえ、速いテストを書く。

このAIルールを使うには
## パッケージの依存の向きを一方通行にし、状態の置き場を決める システムが成長しても所有権、依存方向、状態の配置を予測できるように、コードベースを整理します。 ### 責務別に構造化する - すべてのコンポーネント、フック、ストア、サービス、パッケージに明確な目的を一つだけ持たせ、すでに複雑な単位へ分岐やモードを追加するより、小さな単位を組み合わせることを優先します。 - 依存グラフではアプリケーションコードを再利用可能なパッケージより上位に置きます。下位レベルのパッケージはアプリケーションモジュールをインポートしてはいけません。 - 分離されたサービスには、無関係なUIフレームワーク、レジストリ、プロバイダーSDK、実行エンジンを持ち込まないようにします。 - アプリケーションごとに認証を再実装せず、互換性のある一つのセッションとデータモデルを通じて共有します。 ### 型と状態の所有権を維持する - コンポーネントのプロパティ、状態、関数の入力、戻り値に明示的なインターフェースを定義します。 - `any`は使わず、型が本当に不確かな場合は型ガードとともに`unknown`を使います。 - サーバー状態はサーバー状態ライブラリに、グローバルなクライアント状態はストアに、ローカルな表示状態はコンポーネントに保持します。 - 複雑なストアは実装モジュールと型モジュールに分割します。再読み込み後も残す必要がある状態だけを永続化し、永続化するフィールドを明示的に選択します。 ## その場で作り直さず、共通のヘルパーとツールを使う ローカルな代替物を作るのではなく、一つの正規ツールチェーンと共有実装パターンを使用します。 ### 一貫したコード規約を適用する - リポジトリで選定されたパッケージマネージャー、フォーマッター、リンター、型チェッカー、監査コマンドを使用します。 - コンソール出力ではなく構造化ログを使用します。リクエスト単位のルートラッパーがリクエスト識別子を自動的に付与するようにします。 - 一貫した命名とインポート順に従い、装飾的または非構造的なコメントではなく、型のみのインポート、定数アサーション、明示的な参照要素型、TSDocを使用します。 - 絶対インポートを優先します。フォルダーに少なくとも3個のエクスポートがある場合だけバレルを作成し、バレル以外のファイルから再エクスポートしないでください。 ### 実績のあるヘルパーを再利用する - ID生成、遅延、エラー正規化、メッセージ抽出、ディープクローン、オブジェクトのフィルタリング、切り詰め、ジッター付きリトライバックオフには共有ヘルパーを使い、ローカルパッケージやその場限りのロジックで作り直さないでください。 - 利用側が1つだけのヘルパーはインライン化し、少なくとも2つのファイルで必要になったらユーティリティモジュールへ移します。 - 約50行を超える、少なくとも2つの呼び出し側で使われる、または状態やロジックを所有する場合は、コンポーネントを抽出します。表示だけを担う10行未満の単一用途フラグメントはインラインのままにします。 ## APIのリクエストとレスポンスを1か所で決め、例外には理由を書く ルートとクライアントのワイヤー形式が互換性を失わないように、すべてのHTTPリクエストとレスポンスを一度だけ定義します。 ### 契約を境界の正本にする - リソースファミリーごとに一つの契約モジュールを保持し、パスパラメーター、クエリ値、ボディ、ヘッダー、レスポンスのスキーマを分けたうえで、一つのルート契約にまとめます。 - クライアント向けに名前付きスキーマと名前付き入出力型エイリアスをエクスポートし、その場しのぎのワイヤー型を導出したり文字列ベースの識別子を再定義したりせず、共有識別子プリミティブを再利用します。 - ドメインバリデーターがスキーマライブラリを直接使うことは許可しますが、ルート境界スキーマはルートハンドラーとクライアントの外に置きます。 ### 例外を明示的に管理する - 例外的なraw fetch、二重キャスト、生のJSON読み取り、意図的に型を付けないレスポンスの直前には、理由を示す注釈を必須とします。 - raw fetchは、ストリーミング、バイナリ転送、マルチパートアップロード、署名付きURL、OAuthリダイレクト、外部オリジンなどの場合だけ許可します。 - 生のJSON読み取りは標準パーサーを使えないエンベロープまたは寛容な解析にだけ許可し、型なしレスポンスは本当に不透明なデータにだけ許可します。 - 正当なファイル全体の例外は、行単位の回避策として散在させず、レビュー済み許可リストに保持します。 - 理由の欠落、ルートローカルなスキーマ、生の読み取り、未承認のfetch、安全でない二重キャストがあれば、厳格な境界監査を失敗させます。 ## スキーマから順に、APIのルートとクライアントのフックを作る 認証、検証、レスポンスの型、キャッシュが端から端まで一致するよう、境界機能を固定された順序で追加します。 ### 実装順序に従う 1. 契約と、その名前付き入出力型を作成します。 2. ルートごとに再実装せず、共有の認証、レート制限、アクセス制御ミドルウェアを組み合わせ、正規のリクエストコンテキスト兼エラー処理ラッパーでルートを包みます。 3. 信頼できない入力を解析する前に認証と認可を行い、その後、契約を通じてリクエストの全区分を解析します。 4. レスポンススキーマで宣言されたJSON形式を正確に返します。 5. 同じ契約を通じてデータを送信・検証する型付きクライアントフックを追加します。 6. コンポーネントでそのフックを使い、型付きエラーメッセージを表示します。 ### スキーマをマイグレーションのようにレビューする - 省略と`null`を区別します。optionalとnullableを組み合わせると第三の状態が生まれるため、意図的に行う必要があります。 - すべてのレスポンス呼び出し箇所を、宣言されたレスポンススキーマと比較します。 - 文字列、配列、上限、サイズに明示的な境界を設定し、フィールド固有の分かりやすいエラーを用意します。 - unknownスキーマは例外として扱い、ペイロードが不透明な理由を文書化します。ワイヤー形式に判別子がある場合は、判別可能なユニオンを優先します。 - 意味上の品質には人によるレビューを使います。構造監査だけでは、弱い境界や不正確なレスポンス形式をすべて検出できません。 ## キャンセルや楽観的更新の後も、クエリのキャッシュをずらさない サーバー状態をコンポーネント状態に隠さず、キャンセル、キャッシュ、無効化、楽観的更新を明示します。 ### 安定したフックとクエリキーを構築する - 安定したコールバックが最新の入力を読む必要がある場合は参照を使い、入力が変わった時にその参照を更新します。 - すべてのクエリ関数からキャンセルシグナルを転送し、すべてのクエリに明示的な鮮度期間を設定します。 - 対象を絞った無効化のため、ルートからリスト、詳細のプレフィックスへ続く階層キーを構築します。以前のデータを保持するのは可変キーのクエリだけにし、静的クエリでは保持しません。 ### ミューテーションを安全に整合させる - 楽観的更新の前に、影響を受けるクエリをキャンセルし、エラー時に復元できるよう以前のキャッシュ値を保存します。 - 成功時だけでなくsettlement時に整合と無効化を行います。成功と失敗の両方でキャッシュの一貫性が必要だからです。 - リソースツリー全体を無効化するより、対象を絞った無効化を優先します。 - 安定したミューテーションメソッドやオブジェクトを、必要もなくコールバックの依存関係へ追加しないでください。 - 同一オリジンのJSONは型付きリクエストヘルパーを通します。raw fetchの例外は、文書化された境界ポリシーに従う必要があります。 ## 共通コンポーネントの見た目は中で決め、プロパティで変える 共有コンポーネントの外観は、各利用側ではなくコンポーネントが所有する単一の正本として扱います。 ### 表示をローカルかつ組み合わせ可能に保つ - ローカル機能のためにグローバルスタイルを変更せず、スタイルをコンポーネントに限定します。 - クラス構築ロジックを重複させず、プロジェクトの条件付きクラスヘルパーと寸法の短縮記法を使います。 - コンポーネントに本物のバリアントが少なくとも2つある場合だけバリアント用ツールを使い、一つの状態切り替えには単純な条件付きクラスを使います。 - 内部サブパスではなく、対応している公開エントリーポイントから共有コンポーネントをインポートします。 - 未定義の変数は意図しない色へ暗黙にフォールバックする可能性があるため、参照するCSS変数が存在することを確認します。 ### プロパティを通じて意図を伝える - エラー、装飾要素、ラベル、ヒント、必須状態、動作モードにはプロパティを公開します。利用側によるクラス上書きはレイアウトとサイズ調整に限定し、正規の境界線、塗り、角丸、高さ、色、フォーカス表現には使わないようにします。 - ほぼ重複したコンポーネントを維持せず、モードには判別可能なユニオンのプロパティを使います。 - モーダル本体の中では正規のラベル付きフィールドラッパーを使い、フィールドの余白をヘッダーとフッターに揃えます。 - 移行後は並行したデザインシステム経路を残さず、旧バリアントを撤去します。 ## 共通のモックと偽タイマーで、速いVitestテストを書く リポジトリ標準のテストランナー、グローバルフィクスチャ、決定論的なタイミングパターンを使用します。 ### 動作を分離する - 各テストには、リポジトリ標準のテスト接尾辞を使い、対応するソースの隣に名前を付けます。 - グローバルモックと共有ファクトリーを再利用し、異なる動作が必要なテストでだけ置き換えます。各テストの前にモック状態をクリアし、テストごとの戻り値を明示的に定義します。 - テスト対象でない場合は、重いレジストリとプロバイダー依存関係をモックします。 - ブラウザーAPIが必要でない限り、サーバーランタイム環境を使用します。 - 実タイマーを避け、偽タイマーまたは動作に必要な最小限の決定論的遅延を使用します。 ### 高コストなモジュール入れ替えを避ける - Vitestでは、ホイストされたモックと静的インポートを優先します。 - モジュールリセット、後置モック、動的インポートを組み合わせず、明示的なモックで十分な場合は`importActual`を通じて実装本体をインポートしないでください。 - 後置モックを隠すヘルパーを呼び出さず、認証とログのモックを明示的に保ちます。 - レビュー前に、関連テストとともに厳格なAPI境界監査を実行します。 ## 統合機能を、変数が決まる前に型を変えずに追加する 依存関係の順序で統合機能を構築し、実行時まで未解決の参照を維持します。 ### 統合の順序に従う 1. 安定した`snake_case`識別子でツールを定義し、登録します。 2. 利用側ブロックを追加し、正規の順序で登録します。 3. アイコンと必要なメタデータを追加します。 4. 統合機能に必要な場合だけトリガーを追加します。 5. 信頼できるAPI文書に照らしてサービスの動作を検証します。 ### ライフサイクルのタイミングを尊重する - シリアライズ時の設定と、実行時のパラメーター解決を区別します。 - 未解決の値をシリアライズする時に型を強制変換しないでください。動的参照を壊す可能性があります。 - 数値などの型変換は、実行時に変数が解決された後だけ行います。 - ツール、ブロック、トリガー、認証、ファイル入力のメタデータを同期させます。 - lint、型チェック、テスト、厳格な境界監査を実行し、その後、契約、キャッシュ、スタイル、レジストリの一貫性について最終差分をレビューします。