API 設計
他人が使う契約をつくる
API は一度公開すると簡単には変えられない契約になる。リソースの切り方、同じリクエストが2回届いたときの振る舞い、失敗の伝え方。これらは実装の都合ではなく、呼ぶ側が安全なコードを書けるかどうかで決まる。
読み終わると、これに答えられるようになります
- ?決済 API に同じリクエストが2回届いたらどうなるべきか
- ?エラーレスポンスに何を入れると呼び出し側が自動復旧できるのか
- ?GraphQL にすると何が楽になり、何が難しくなるのか
リソース設計 — 一度公開したら変えられない契約
URL は動詞ではなく名詞で切る、の理由。そして「きれいな REST」に収まらない操作をどう扱うか。
レスポンスの1フィールドを削除したら、把握していなかった3つのクライアントが壊れた
冪等性 — 同じリクエストが2回届く前提で設計する
ネットワークがある限り、再送は異常ではなく正常系。2回実行されても壊れない形をどう作るか。
決済 API のタイムアウト後にリトライし、同じ注文で2回課金された
エラー設計 — 呼び出し側が正しく振る舞える情報を返す
エラーは失敗の通知ではなく、次に何をすべきかの指示。リトライしてよいか、直せば通るかを伝える。
すべてのエラーを 200 + { success: false } で返し、クライアントが一時障害を延々とリトライした
REST と GraphQL — 何を楽にして、何を難しくするか
過不足のない取得という利点の裏で、キャッシュ・認可・負荷制御がクライアント任せから自分の責任に移る。
GraphQL 導入後、CDN キャッシュが一切効かなくなり、オリジンの負荷が10倍になった
ページネーション — 数えて飛ばすか、続きから取るか
offset は「ページ番号」という画面の都合から来ている。件数が増えると遅くなり、データが動くと境界がずれる。カーソル方式が何を解いて、何を捨てるのか。
offset で一覧を書き出すバッチが、毎回数件ずつ静かに取りこぼしていた
リトライとタイムアウト — 助けるつもりの再試行が、障害を大きくする
決済サービスは2分で直ったのに、全体は40分止まった。層ごとのリトライは掛け算になり、揃っていないタイムアウトは誰も待っていない仕事を増やす。
決済サービスが2分遅くなっただけで、3層のリトライが呼び出しを27倍にし、全体が40分止まった
非同期処理とキュー — 「受け付けました」のあとで、何が起きているか
メール送信をキューに移したら、同じメールが4通届く人と、1通も届かない人が出た。キューは「必ず1回」を約束しない。重複と消失は、処理を書く側が引き受ける。
メール送信をキューに移したら、注文確認メールが4通届く人と、1通も届かない人が出た
レート制限 — 誰を、どの単位で、どこで止めるか
ログインに IP ごとの制限をかけたら、同じ会社の社員がまとめて締め出され、数千の IP に散らばった攻撃は素通りした。制限の強さより先に、何を1人と数えるかを決める。
IP ごとのログイン制限で、同じ会社の社員がまとめて締め出され、分散した攻撃は素通りした
API のバージョニング — 変えられないものを、どう変えるか
レスポンスから使っていないフィールドを1つ消した。翌日、古いアプリが全部落ちた。自分が使っていないことと、誰も使っていないことは違う。
使っていないフィールドを消したら、更新していない利用者のアプリが全部落ちた
ファイルのアップロード — サーバーを通すか、通さないか
動画の投稿を許したら、アプリのメモリが枯れて全機能が落ちた。1人が 800MB を送っただけだった。受け取り方を決める前に、どこを通すかを決める。
動画の投稿を許した日、1人が 800MB を送ってアプリ全体がメモリ不足で落ちた
2 つのシステムに書く — 片方だけ成功した状態を、先に設計しておく
決済は成功した。自社の DB には「未払い」と残った。利用者はお金を払ったのに使えなかった。自社の DB と外部のサービスは、1 つのトランザクションにならない。だから、途中で止まったときにどちらへ倒れるかを、先に決めておく。
決済は成功したのに、直後のデプロイで処理が止まり、自社の DB には「未払い」が残った。37 人が、払ったのに使えなかった