building-backends
これはバックエンド作業の基盤となるスキルです。サーバーコードを書く前に必ず読んでください。すべてのサービスが一貫性を持つように、デフォルトと譲れないルールを設定します。その後、詳細な手順は各スキルに引き継がれます。ここでのルールがローカルリポジトリの慣習と矛盾する場合は、リポジトリが優先されます。矛盾しない場合は、このルールに従ってください。
まず、構築する前に理解する
- コードを追加する前に、既存のコードを読むこと。 リポジトリのフレームワーク、エラーモデル、レイヤリング、命名規則、テストスタイルに一致させること。コードベースと戦う「正しい」パターンは間違ったパターンです。リポジトリが既に行っていることを別の方法で実装することは、改善ではなく欠陥です。
- 契約(コントラクト)を明確にし、実装は後回しにする。 設計を選択する前に、入力、出力、エラーケース、呼び出し元、一貫性/レイテンシの期待値を把握すること。バックエンドの手戻りのほとんどは、間違った契約を急いで構築することから生じます。
- 退屈な方法を選ぶ。 巧妙な解決策よりも、実績があり、読みやすく、明白な解決策を選ぶこと。バックエンドは午前3時に無人で動作します。設計の優雅さよりも、半分眠っているオンコールエンジニアが理解できることを最適化してください。
アーキテクチャ — ルール
- 依存関係が内向きになるようにレイヤーを構成する。
トランスポート(http)→ サービス(ユースケース)→ ドメイン(エンティティ/ルール)、およびサービス → リポジトリ(I/O)→ ドメイン。ドメインは外部に何もインポートせず、I/Oも行いません。インポートの矢印が逆向きであれば、設計は壊れています。 - ハンドラーは調整し、サービスは決定し、リポジトリはI/Oを行う。 ハンドラーは入力の解析/検証、サービスの呼び出し、結果のレスポンスへのマッピングを行います — ビジネスロジックは含みません。リポジトリは、データベース、キュー、外部HTTPに触れる唯一の場所です。
- ドメインをインフラストラクチャなしでテスト可能に保つ。 データベースやネットワークなしでコアロジックの単体テストができない場合、ロジックが間違ったレイヤーにあります。
- コンポジションルートは1つ。 設定の構築、依存関係の注入、サーバーの起動を1つの場所で行います。その他はすべて依存関係を受け取ります。グローバルなシングルトンを構築するために外部に手を伸ばすことはありません。
- ステートレスに設計する。 セッション/スクラッチ状態はプロセスメモリではなく、共有ストア(データベース/キャッシュ)に置きます。これによりサービスは水平方向にスケーリングし、再起動後も存続します。モジュールレベルの変数でキャッシュしている場合は、立ち止まって考え直してください。
→ このレイヤーがあらかじめ配線されたスケルトンを立ち上げるには、api-service-scaffold を使用します。
API設計 — ルール
- 明示的でバージョン管理された契約。 初日からAPIにバージョン(URLまたはヘッダー)を付けます。出荷済みの契約に破壊的な変更を決して加えないでください — 新しい形状を追加し、古いものを非推奨にします。
- すべての入力を境界で検証し、型付けされた値にする。 コアは生のリクエストボディを決して見ません。無効な入力は422とフィールドレベルの詳細で拒否します。クライアントを決して信頼しないでください。
- エラーモデルは1つ、マッピングは1か所。 ドメインコードは型付けされたエラー(
NotFoundError、ConflictError、ValidationError、UnauthorizedError...)を発生させます。単一の境界マッパーがそれらをステータス +{ code, message, details? }に変換します。スタックトレースや内部メッセージを決して漏洩させないでください。404は「見つからない」と「あなたのものではない」の両方に使用します — これらを区別すると存在情報が漏洩します。 - 一貫した形状。 成功時もエラー時も同じエンベロープ、一貫したページネーション(大規模セットではオフセットよりもカーソル)、一貫したフィールドのケーシング。呼び出し元は一度規則を学べば十分です。
- 安全でない操作に対する冪等性。
PUT/DELETEは定義上冪等です。冪等性キーを使用して、再試行可能なPOSTを安全にします。ネットワークは再試行します — 顧客に二重請求が発生するのは設計のバグです。 - 正しいステータスコード。 2xxは成功、4xxは呼び出し元の過失、5xxはあなたの過失です。検証エラーは422であり、エラーボディ付きの200ではありません。認証エラーは400ではなく401/403です。
データレイヤー — ルール
- データベースが信頼できる唯一の情報源であり、不変条件をそこで強制する。 外部キー、一意制約、
NOT NULL、チェックはスキーマに属し、アプリケーションコードだけで実装してはいけません — アプリケーションコードにはバグや競合状態がありますが、データベースは嘘をつきません。 - トランザクションは不変条件をラップし、可能な限り短くする。 オールオアナッシングでなければならないものはすべて1つのトランザクションに含めます。トランザクションを保持している間は、遅い処理(外部呼び出し、重い計算)を行わないでください。長時間のトランザクションはロックを保持し、他のすべてを飢餓状態にします。
- 並行性を想定する。 2つのリクエストが同時に同じ行にヒットします。楽観的ロック(バージョンカラム)または適切な分離レベルを使用します。往復の間、保護なしで読み取り→変更→書き込みを行わないでください。
- マイグレーションは後方互換性があり、ロールバック可能であること。 1回のデプロイでカラムの意味を変更しないでください。拡張→移行→縮小の順に行います。ホットテーブルでテーブルブロッキングロックを取得しないでください。すべてのマイグレーションにはテスト済みのロールバックが付属しています。
- N+1問題に注意する。 関連データはクエリのループではなく、セットで取得します。実際のエンドポイントでのクエリ数をレイテンシだけでなくプロファイルしてください。
- 実際に実行するクエリにインデックスを作成します — そしてそれだけにします。未使用のインデックスは書き込み増幅に過ぎません。
→ マイグレーションの仕組み(ロック、拡張-縮小、バックフィル、ロールバック)については、db-migration-guardian を使用します。
セキュリティ — 基本(譲れない)
- すべてのリクエストに対して、サーバーサイドで認証してから認可する。 クライアントが自分が誰であるか、何をしてもよいかという主張を決して信頼しないでください。すべてのリソースアクセスで所有権を確認します — 「有効なトークンを持っている」≠「このレコードは私のものだ」。
- すべてのクエリをパラメータ化する。 文字列で構築されたSQLは決して使用しません。同じことは、注入可能なインターフェース(シェル、LDAP、テンプレート)にも当てはまります。
- 秘密情報は環境/シークレットマネージャーから取得し、コードからは取得しない。 ソース、ログ、エラーメッセージに機密情報を含めないでください。(
secrets-managementスキルを参照。) - 最小権限の原則をあらゆる場所で守る — データベースユーザー、サービス資格情報、クラウドロールは、サービスが必要とするものに正確にスコープを限定します。
- 受信エンドポイントと送信呼び出しにレート制限とタイムアウトを設定する。 無制限の呼び出しや制限のないエンドポイントは、発生を待っているサービス拒否攻撃です。
- 境界で検証とエンコードを行い、インジェクションとSSRFを防ぐ。 他の内部サービスからのものを含め、すべての外部入力を信頼できないものとして扱います。
並行性、ジョブ、および障害
- すべての外部呼び出しは失敗すると想定する。 タイムアウトを設定し、バックオフ+ジッターを使用した制限付き再試行と、不安定な依存関係に対するサーキットブレーカーを追加します。タイムアウトなしの再試行は、1つの遅い依存関係がサービス全体をダウンさせる方法です。
- 操作を冪等にする ことで、再試行(自分または呼び出し元の)が安全であることを保証します。
- バックグラウンド処理はキューに入れ、リクエストパスでは処理しない。 リクエストはキューに入れます。ワーカーが処理します。障害は、サイレントな
catch {}ではなく、調査のためにデッドレターキューに送られます。 - 大きな声で失敗し、グレースフルに低下する。 エラーをテレメトリに公開します。重要でない依存関係がダウンしている場合は、リクエスト全体を500にするのではなく、低下(古いデータを提供、エンリッチメントをスキップ)します。
可観測性は、後付けではなく「完了」の一部である
プロダクションでデバッグできないサービスは完成していません。構造化ログ、RED/USEメトリクス、および1つの相関IDに結び付けられたトレーシングは、最初のインシデントの後ではなく、構築する際に組み込みます。
→ インストルメンテーションの詳細とカーディナリティ/PIIガードレールについては、backend-observability を使用します。
テストの姿勢
- パブリックインターフェース(エンドポイント/ユースケース)を通じて振る舞いをテストし、プライベートな内部はテストしない — これによりテストはリファクタリング後も存続します。
- ピラミッドであって、アイスクリームコーンではない: ドメイン/サービスロジックに対する多数の高速な単体テスト、実際のデータベースと配線に対する焦点を絞った統合テストセット、クリティカルパスに対する少数のエンドツーエンドテスト。
- すべてのバグ修正には回帰テストを含める — 修正前に失敗し、修正後に合格することを確認します。
- 決定論的なテスト。 壁時計、ネットワーク、順序に依存しないこと。データは明示的にシードし、テストは互いに分離します。
バックエンドタスクの手順
- 周囲のコードを読み、契約(入力/出力/エラー/呼び出し元/一貫性)を確定します。
- 作業を適切なレイヤーに配置します。ドメインをI/Oフリーに保ちます。
- エッジで入力を検証します。単一のエラーモデルを使用します。正しいステータスを返します。
- 不変条件をデータベースと短いトランザクションに配置します。並行性を処理します。マイグレーションを拡張-縮小として計画します。
- セキュリティベースライン(認可+所有権、パラメータ化クエリ、秘密情報、タイムアウト/制限)を適用します。
- 外部呼び出しとジョブを耐障害性と冪等性を持つものにします。
- 構築しながらインストルメント化します。適切なレイヤーにテストを追加します。バグの回帰テストも含めます。
- 関連する特定のスキルの「完了の定義」と照らし合わせて確認します。
完了の定義
- レイヤーが尊重されている(ドメインはI/Oを行わず、依存関係は内向き)。コンポジションルートは1つ。
- すべての入力が境界で検証されている。エラーモデルは1つで、1か所でマッピングされている。正しいステータスコード。バージョン管理された契約。
- 不変条件がデータベースで強制されている。トランザクションは短い。並行性が処理されている。マイグレーションは後方互換性があり、ロールバック可能。
- セキュリティベースラインが満たされている:認可+所有権チェック、パラメータ化クエリ、秘密情報は外部、タイムアウト/レート制限が存在する。
- 外部呼び出し/ジョブは冪等で耐障害性がある。バックグラウンド処理はリクエストパスの外にある。
- 可観測性が配線されている(相関IDに基づくログ/メトリクス/トレース)。適切なレイヤーにテストがあり、グリーンで、回帰カバレッジがある。