世界を動かす技術を、日本語で。

良いAPIデザインについて私が知っているすべてのこと

概要

  • API設計 は、現代のソフトウェア開発で中心的な役割を果たす要素
  • 「ユーザースペースを壊さない」 ことがAPI設計の絶対的原則
  • バージョニング は最終手段として利用し、頻繁な破壊的変更は避けるべき
  • APIの質 は製品の価値に依存し、API単体の優秀さだけでは成功しない
  • 認証・冪等性 など、実装上の具体的な注意点も重要

現代ソフトウェアとAPI設計の基本

  • API は、プログラム同士が通信するための 公開インターフェース
  • RESTやGraphQL、CLIツールなど、様々な形態でAPIを設計・利用経験
  • API設計 は「 親しみやすさ」と「 柔軟性」のバランスが重要課題
    • シンプルで直感的なAPI設計
    • 長期的な拡張性を考慮した工夫
  • API利用者 はAPI自体ではなく、APIを通じて 目的を達成 したいだけ
  • 理想的なAPI は、 説明書を読む前から使い方が想像できる レベルの親しみやすさ

APIの互換性維持と変更の原則

  • APIの変更 は、利用者のソフトウェアを壊すリスクが高い
  • 追加的な変更 (例:レスポンスに新フィールド追加)は基本的に安全
  • 削除や型変更、構造変更絶対に避けるべき
    • 既存のコードが即座に動かなくなるリスク
  • 「WE DO NOT BREAK USERSPACE」 という原則の厳守
    • 上流APIの軽率な変更が、下流の多数のソフトウェアに甚大な影響
    • 些細な理由(例:綴りミス修正)でも互換性重視

バージョニングによる互換性維持

  • 破壊的変更 がどうしても必要な場合は バージョニング を利用
    • 例: /v1//v2/ といったURLパスによるバージョン管理
    • Stripe はヘッダーでバージョン管理、UIでデフォルトバージョン設定
  • バージョニングの課題
    • ユーザーがドキュメントや実装で混乱しやすい
    • メンテナンスコスト増大(エンドポイント数が増える)
    • コアロジックにバージョン依存の条件分岐が増え、抽象化の限界
  • バージョニングは最後の手段 として利用

APIの価値と製品力の関係

  • API単体 では価値が生まれず、 製品自体の魅力 が最重要
  • FacebookやJira のAPIは使いにくくても、製品が必要とされるため利用される
  • APIの品質 は「類似製品間の選択」でのみ決定的な差別化要素
  • APIが存在しない製品 は、技術ユーザーから敬遠される

製品設計とAPI設計の連動

  • 製品設計が悪いと、API設計も歪む
    • 例:コメントをリンクリストで管理しているブログシステム
      • APIレスポンスも冗長・非効率になりやすい
    • 技術的制約がUIでは隠せても、APIでは露呈しやすい
  • API設計は、製品の「基本リソース」設計に依存

認証と使いやすさ

  • 長期有効なAPIキー による認証のサポート推奨
    • OAuth などの高度な認証も必要だが、まずはAPIキーで簡単に始められることが重要
  • API利用者 は必ずしもプロのエンジニアとは限らない
    • セールス、プロダクトマネージャー、学生、趣味ユーザーなど多様
  • 複雑な認証フロー (例:OAuthハンドシェイク)は多くのユーザーにとって障壁

冪等性とリトライ設計

  • APIリクエストが失敗 した場合、何が起こったか分からないケースがある
    • 例:500エラーやタイムアウト時、操作が実行されたか不明
  • 冪等性(idempotency) の実現が重要
    • 同じリクエストを複数回送っても結果が重複しない設計
    • idempotency key (ユーザー定義の文字列)をリクエストに含める方式推奨
  • 冪等性 は、金銭移動や医療など重大な操作で特に必須

必要に応じて、各トピックごとにさらに詳細な解説や具体例を追記可能です。

Hackerたちの意見

作者はバージョンベースのAPIがあまり好きじゃないみたいだけど、アプリケーションの最初からそれを組み込むことをおすすめするよ。未来を予測するのは無理だし、誰かや何かのせいで壊れる変更が強いられる可能性が高いからね。

もし将来的に壊れる変更が強いられたら、関数の名前を変えればいいんじゃない?

後からバージョン管理を追加することに害はないと思うよ。例えば、あなたのAPIが/api/postsだとしたら、次のバージョンは単に/api/v2/postsになるだけだよ。

著者が「v1」を追加しない方がいいって言ってるのには同意だな。実際、APIが成長するとどうなるかというと、まずチームは既存のエンドポイントをできるだけ拡張して、新しいフィールドやオプションを追加するけど、互換性を壊さないようにする。で、もし後方互換性がない操作が必要になったら、エンドポイントの名前も見直したくなるから、新しい名前で新しいエンドポイントを作ることが多いんだ。(「v2」とか名付けるんじゃなくてね。)それから、もし全体のAPIを再構築する必要が出てきたら、チームはサービス/API全体を非推奨にして、新しい名前でより良いサービスを立ち上げることが多い。だから、結局「/v2」が名前に入ってるエンドポイントは本当に稀なんだ。業界に25年いるけど、「/v1」に対して「/v2」を持ってるサービスを見たのは一度だけだよ。

著者が最初にエンドポイントに「/v1」を含めないって意味したとは思わない。要は「/v2」を持たないように全力を尽くすべきだってことだよ。なぜなら、バグ修正のたびに2つのバージョンを維持しなきゃいけなくなるから、同じコード変更を2か所で行うか、既存の条件ロジックや新しい条件ロジックに対して余分な条件ロジックを掛け算しなきゃいけなくなる。複数のバージョンをサポートするコードベースはスパゲッティコードみたいになって、通常は「/v1」が将来の互換性を考慮して設計されていなかったことを意味するんだ。

反対だな。最初からバージョン管理を組み込むと、使われる可能性が高くなるけど、それって良くないことだよ。

「ユーザースペースを壊さない」というリマインダーは貴重だけど、よく見落とされがちだよね…あ、Spotify、Reddit、Twitterが思い浮かぶ。

ここで唯一同意できないのは、内部ユーザーもただのユーザーだってことかな。確かに彼らは技術的なことに詳しいかもしれないし、他のプログラマーかもしれないけど、忙しいことには変わりないよ。自分のプロジェクトを作ってることが多いし、あなたのAPIの変更に対応する時間や能力がないこともある。できるだけ時間をかけて、自分たちでAPIを使ってみてから他の人に開放した方がいいよ。一度開放したら、もう動かせないし、「ユーザースペースを壊さない」という契約を守らなきゃいけないからね。

バージョン管理はこの問題を解決するのに役立つと思うよ。内部ユーザーに負担をかけないためにできることはたくさんあるけど、最も効果的なのは仕様について協力して、作業中のコピーを関係者に提供することだね。たとえそれが生きた文書でも、彼らに基準を持たせるのはすごく助けになるよ(ただし、オフィスの人間関係が進行中の部分で問題を起こさないようにしてくれるならだけど)。

内部ユーザーの場合、移行を手伝うためのインスツルメンテーションがあるだろうから、実際にAPIバージョンをサンセットさせることができる。APIバージョニングは魅力的な解決策になるよ。私はAPIバージョニングに参加したこともあれば、デフォルトで使わない組織で使われているのを観察したこともある。

「ユーザースペースを壊さない」というリマインダーはいいけど、その言葉のもう一つの側面をみんな忘れがちだよね。「カーネルAPIは警告なしに壊すことができるし、壊すつもりだ」ということ。これは、リマインダーが「誰かを壊すようにAPIを変更しない」ということではなく、より微妙な「安定しているものを宣言し、それを壊さない」ということを示してるんだ。

そうそう、有名な話だけど、Linuxには安定した公開ドライバーAPIがないんだよね。これがGoogleのFuchsia OSの動機になったと思う。Linuxはユーザースペースとハードウェアの両方に対して意見を持ってるけど、逆の方向にね。

カーネルがユーザースペースを壊さなくても、GNU libcは常に壊してるから、結局Linuxのユーザースペースはカーネルのメンテナンスに関係なく壊れちゃうんだよね。簡単に言うと、新しいlibc用にコンパイルされたプログラムやライブラリは、古いlibcではABI互換じゃなかったり、全く動かなかったりするから、全部を一緒にアップグレードしないといけない。ちょっと皮肉で面白いのは、Windowsが数十年前にこの問題を再配布可能パッケージで解決したことだね。

カーソルベースのページネーションが話題に出たね。もう一つの便利な機能があって、ユーザーがページを読み込んでから「次へ」ボタンを押すまでにアイテムが追加された場合、インデックスベースのページネーションだと前のページの既に見たアイテムが表示されちゃう。でも、カーソルベースのページネーション(前のページの最後のオブジェクトのIDを使う)だと、まだ見てない新しいアイテムのリストがもらえる。これは無限スクロールに役立つよ。ただし、カーソルベースのページネーションの欠点は、ページNにジャンプするボタンを作るのが難しいことだね。

カーソルは不透明にして、データベースのサイズを決して明らかにしないようにすべきだよ。不透明にすると、カーソル自体に追加の状態をエンコードすることができる:検索パラメータ、ウォームキャッシュ/ルーティングトポロジーなど。

今日「API」を見る人は、大体「リクエストを送るウェブアプリで、いくつかの引数を渡してヘッダーを設定して、返ってきたヘッダーから設定を確認して、返ってきたデータを解析する」って思ってる。でも「API」ってのは「アプリケーションプログラミングインターフェース」の略なんだ。元々はアプリケーションプログラムのためのもので、つまり…ユーザーインターフェースを持つプログラムのこと!1940年代に由来があって、1990年までは他のことにはあまり言及されてなかった。APIは80年以上も存在してるんだ。今これを読んでる人たちよりも古い本や論文もたくさん出てる。昔のAPIはどんな感じだったんだろう?何を使ってたの?目的は何だったの?プログラマーたちはどうやって問題を解決してたんだろう?それが君にどう関係するか、考えてみて。

どうやってキーを保存すべきか?耐久性のあるリソース特有の方法(例えば、コメントテーブルのカラムとして)で保存している人を見たけど、それが絶対必要とは思わない。一番簡単なのは、Redisや似たようなキー/バリューストアに入れることだね(イデポテントキーをキーとして)。Redisにキーを保存することで、全ての失敗ケースでイデポテント性が達成できるのかはわからないけど、アルゴリズムはどうなってるの?リクエストを処理しているサーバーが条件付き書き込み(例えば、SET key 1 NX)をして、キーが既に保存されているのを見たらどうするの?コメントを作成するのをスキップするの?コメントが以前に作成されたとは仮定できないよね。なぜなら、キーをRedisに保存してから実際にデータベースにコメントを作成するまでの間にプロセスが終了してしまう可能性があるから。イデポテントキーを保存しようとする試みは、操作ペイロードと一緒に原子的にコミットされる必要がある(失敗した場合はロールバック)。つまり、常にリソース特有のIDである必要がある。実際のところ、イデポテントキーは実行される操作(リクエスト)のIDで、例えば「コメント作成」や「コメント更新」などだね。

そうそう、冪等性を導入するために別のコンポーネントを追加しないでほしい。理解していないと、変な抽象化が漏れたり、単に壊れたりする可能性が高いから。書き込みにラベルやメタデータをサポートする方がずっといいよ。そうすれば、ユーザーは自分の進捗を追跡できて、既存のデータと一緒に保存できるから。

あなたのユーザーの多くはプロのエンジニアではないかもしれません。営業マンやプロダクトマネージャー、学生、趣味でやってる人たちなど、いろんな人がいるんです。これは認証だけの話じゃないよ。ビジネスの現場で働いていると、あなたのAPIは本当にランダムなユーザーに使われることになるからね。彼らはPythonでAPIを呼び出す方法をググれるかもしれないけど、UTCを自分のローカルタイムゾーンに変換することはできないかもしれない。

冪等性キーをRedisに保存することを提案してるね。可能なら、書き込みのトランザクションで、書き込みミューテーションと一緒に保存するのがいいと思う。

こちらもいいおすすめがあるよ: https://jcs.org/2023/07/12/api