良いAPI設計の全てを語ろう!
引用元:https://news.ycombinator.com/item?id=45006801
「ユーザー空間を壊すな」って原則は良いけど、カーネル API は警告なしで壊すって、その裏側も忘れないでね。API を壊すなってより、安定な部分をちゃんと宣言して、そこを壊さないってのが大事なんだよ。
カーネルがユーザー空間を壊さなくても、GNU libc はいつも壊しちゃうから、Linux のユーザー空間は結局壊れるの。新しい libc でビルドしたプログラムは古い libc では動かないし。Windows が再配布可能パッケージでこの問題を数十年前に解決したのは、ちょっと皮肉だよね。
GNU libc の後方互換性は結構良いよ。もし幅広いバージョンで動かしたいなら、頑張って古い libc にリンクするといい。でも GUI ライブラリとかはもっと厄介で、互換性が壊れやすいし、古いバージョンはディストリビューションからなくなるし、全部アプリに同梱してもプロトコルの問題が出たりするんだよね。
君は全然違う二つのことを言ってるよ。必要なライブラリバージョン1.9のプログラムが1.0で動かないのは当たり前。glibc は2000年の2.2以降、バイナリ互換性がすごく良いんだ。Linux の「ユーザーランドを壊さない」ほどじゃないけど、かなり近いよ。でも実は Linux も過去にはその約束を破ってる。2.4から2.6への移行とかはひどかったね。古い静的リンクのバイナリは、ファイルシステムの変化で動かなくなることもあるよ。
glibc の API/ABI の変更履歴は、このサイトで確認できるよ。2023年以降は更新されてないけど、各バージョンでどのシンボルが削除されたかの傾向はわかるはずさ。
URL: https://abi-laboratory.pro/?view=timeline&l=glibc
ウェブサイトが glibc 2.34 で24個のシンボルが削除されたって言ってるけど、チェンジログ見たら、ほとんどは後方互換性のためにシンボルが残ってるか、デバッグツール関連で、リンクの問題は起こさないみたいだよ。案外、大したことないように思えるな。
僕が知ってる中で一番ひどい削除は crypt だね。でもそれも、再リンクできないなら適切な LD_PRELOAD を追加するだけで対処できるんだ。
「glibc のバイナリ互換性は2000年の2.2以降、すごく良い」って話だけど、最近、実行可能なスタックを宣言したライブラリのロードを壊しちゃったんだ。たとえ使ってなくてもね。しかも、この後方互換性の問題を直す計画もないみたいだよ。
その一方で、静的リンクされた実行ファイルは、すごく安定してるんだ。その選択肢があるのは良いことだね。
俺の理解だと、GNUのlibc.aをソースコード非公開で静的リンクするのはLGPL違反らしい。これって、Linux上でプロプライエタリソフトウェアを動かしてる会社の95%くらいがやばいんじゃないかな。musl libcはもっと許諾型のライセンスだけど、GNU libcより性能が悪いって聞くね。LLVM libc[1]に期待したいな。そしたらコンパイラドライバーからC/C++標準ライブラリまで、ツールチェイン全部がClang/LLVMになるし、ユーザーコードからlibc実装まで丸ごと最適化して、デッドコードも消して、バイナリサイズも小さくできたら最高だろ![1]: https://libc.llvm.org/
俺が知る限り、LGPLの下でglibcを静的リンクするのは技術的には合法だよ。ただし、アプリのオブジェクトコードのコピーと、ユーザーが別のglibcと再リンクする方法の説明を含める必要がある。.oファイルのソースは必要ない。でも、実際にこれをやってる人は見たことないな。
静的リンクするなら、やっぱりMuslの方が良い選択肢だと思うよ。GNU libcはいくつかの重要な機能で動的リンクに依存してるからね。
Windowsの再頒布可能ファイルって、ユーザーとしてはマジでウザいんだよね。昔アプリを使うたびにMicrosoftの公式ページでダウンロードしろって言われて、正しいボタンを見つけるのが大変だったのを覚えてるよ。ユーザーに負担を押し付けてるみたいで最悪だった。
多くのインストーラーはちゃんとやってくれるから、ユーザーが自分でやる必要はないんだよ。
GNU LibCはどのみち静的リンクするのがすごく難しいんだ(getaddrinfoとかね)。ほとんどの人はmuslを使ってるけど、中にはuclibcを使う人もいるよ。Muslは一部で性能的な欠点があるとしても、実際にはかなり良い選択肢だね。
アプリケーションと一緒に、特定のlibc.soを配布するっていう手もあるよ(実質同じことだけど)。GNU maximalists以外で、これでアプリが(L)GPLに感染するなんて思ってる奴はいないんじゃないかな。
ld.soも一緒に配布しないとダメだよ。そうしないとldとlibcの非互換性の問題にぶつかるからね。
…それにld.soは特定の絶対パスにないとダメなんだ。だからアプリと一緒に配布するだけじゃなくて、ちゃんとしたコンテナを使わないといけないよ。
LGPLライブラリをアプリのソースコードなしで配布するのがダメって思ってるやつは、LGPLを全く分かってないんだよ。そういう人に限って、”GNU maximalist”とかいうおかしな言葉を使うんだから。
あ、前のコメント、俺の言葉選びが悪かったわ。LGPLライブラリをアプリのソースコードなしで配布するのが「禁止」って思ってるやつは、LGPLを全く分かってないって言いたかったんだよ。
Linuxには安定した公開ドライバAPIがないって有名だよね。GoogleのFuschiaもそれが動機だったはず。Linuxはユーザースペースとハードウェアの両方で意見が偏ってる、でも真逆のやり方でさ。
これは単なる意見の押し付けじゃないんだ。カーネルがモジュールをgitツリーに入れない人たちへの梃子なんだよ。彼らは置き去りにされて、常に動くターゲットを追いかけるためにメンテナーにお金を払う羽目になる。解決策はコードを貢献することだよ。
ソフトウェアエンジニアリングでは「インターフェース、実装ではない」って昔から言われてる(Uncle Bobが教えてた)。これは「ユーザースペースを壊さない」の一般化で、実装に依存せずインターフェースを宣言すること。C++ではインターフェースを型として使う。LinuxはCコードベースだけど、構造体へのポインタを渡してカーネルが自由に使う。これが彼らの”オブジェクト指向プログラミング”だよ。
これを聞くとEvanが言ってたのを思い出すな。2から3への移行パスは提供したんだけど、内部変更が多すぎてたくさんのプラグインが壊れちゃったって話。
作者はバージョンベースAPIをあまり好きじゃないみたいだけど、俺はアプリの最初から組み込むことを常に推奨するね。未来は予測できないし、いつか自分じゃコントロールできない誰かに破壊的変更を強いられることになるからな。
もし将来、破壊的変更を強いられるなら、関数に違う名前を使えばいいんじゃない?
REST APIなら、バージョンベースAPIを最初から組み込む必要は別にないよ。application/vnd.foobarってクライアントがリクエストしても、後からapplication/vnd.foo.bar;version=2を計画なしで追加できるからね。
バージョニングを後から追加することに何の害もないと思うな。例えばAPIが/api/postsなら、次のバージョンはシンプルに/api/v2/postsにすればいいだけだ。
Discoverability。/v1/downloadFileや/v2/downloadFileみたいにバージョンを付けた方が、/v3がないかチェックするのも楽だよね。/api/downloadFile、/api/downloadFileOver2gb、/api/downloadSignedFileみたいにすると大変だよ。
これは下流の問題だよ。インテグレーターがv1にバージョン番号を入れるのを強制されてなかったから、v2を使うときの改修コストが、最初からバージョン番号があればかからなかった分だけ高くなるんだ。
もっとコメントを表示(1)
ほとんどのREST APIは、それ(バージョニング)をサポートしてないよ。だから、すでにリクエストタイプが指定されてるAPIなら、バージョニングなんて必要ないんじゃないかな。
俺も筆者の意見に賛成。v1って付けてもめったに役に立たないよ。
APIが成長したら、まず互換性を壊さずに既存エンドポイントを拡張する。
後方互換性のない変更が必要になったら、エンドポイント名も再検討して、新しい名前のエンドポイントを作る方が多いんだ(v2とはしない)。
API全体を再設計するなら、サービス/API全体を非推奨にして、新しいサービスに置き換える方が多い。
だから、/v2が付くエンドポイントなんて本当に稀。業界25年だけど、/v1と/v2があったサービスは一度しか見たことないな。
これだよ、これ!ファイル内で/v1/ってgrepすれば、すべてのAPIエンドポイントを確認できるし、見落としがないか確かめるのもずっと簡単になるんだ。
サービスで/v2が登場するのって、今まで2回しか見たことないな。
大抵は/v1の全てを破綻宣告して、皆を強制的に/v2に移行させるためだね(それが可能ならだけど)。
OpenAPIジェネレーターみたいなものを使ってて、v2で違うDTOを使いたいなら、あんたの提案したやり方じゃ無理だよ。
API設計の議論で何を言いたいのかよく分かんないな。
APIを設計する人が、何をサポートするかを決めるんだよ。
問題はパラメーター(やヘッダー)で、同じAPIスキーマに縛られちゃうことだよね(名前変更とかできない)。
でも、バージョンのおかげで、うちの仕事では/v1/oauthAppleとか/v1/oauthGoogleみたいな古いAPIを/v2/login/oauth/appleや/v2/login/oauth/googleにリネームできたんだ。見た目もずっと良くなったよ。
それって、アプリケーションの最初から(バージョンを)組み込むってことだよね。でも、君はそんなの必要ないって言ってたじゃん。
筆者は最初から/v1を入れないって言ってるんじゃなくて、/v2を避けるべきってことだよ。だって、/v2が出るとバグ修正も2倍になるし、コードがスパゲッティみたいになるじゃん?それは/v1が未来を見越して設計されてなかったってことだね。
OpenAPIを長年使ってるけど、ヘッダーやメディアタイプでバージョン管理しても、Java、Typescript、Goでコード生成は全然問題ないよ。
バージョン管理されたAPIってのは、特定のバージョンではやり方が一つしかないようにできるから、もうサポートされてない古いやり方を捨てられるってことだよ。昔のシステムを壊さずに、不要になったものをきれいサッパリできるわけだね。
OpenAPIなら、複数のメディアタイプを指定できるよ。
もしv1を長くメンテしたいなら、v2の上で軽いshimとして作り直せばいいんじゃない?
互換性のないAPIに変更するなら、どうせ手直しは必要になるよ。APIを使う側ってのは、普通、複数のバージョンを同時にサポートしたりしないからね。
^/api(/(?!v[0-9]).)?$がv1で、^/api/v2(/.)?$がv2ってことだね。これって実際は問題ないんだよ。ただ、思い通りに綺麗じゃないから、ちょっと気持ち悪いってだけじゃないかな。
「パラメータやヘッダーだとAPIスキーマを変えられない」って主張、意味不明だな。バージョン作る意味ってスキーマ変えることでしょ?URL構造のこと?
ヘッダーを使う利点は同じURLを使えることだよ。REST APIでURLは主キーだから、/v1/foo/1と/v2/foo/1は別のリソースになる。でもapplication/vnd.foo;version=1とversion=2で/foo/1なら同じリソースで相互運用できるんだ。
URL構造を変えたいなら、バージョンなしでも変えられるよ。
賛成できないな。バージョン管理を最初から導入しちゃうと、使われる可能性が高くなるけど、それって良くないことだと思うよ。
著者はバージョン管理を推奨してるんだけど、「APIを責任を持って変更する方法」としてだよ。でも、新しいバージョンに切り替えるのは最終手段にすべきだって言ってるんだよね。
著者は「v1」を追加するなって言ってないよ。バージョン管理はAPIを責任を持って変える方法だって言ってるけど、最終手段としてだけやるべきだってさ。
だから「v1」を追加して、いざって時にv2に上げられるようにしておくけど、できる限りv2に上げるのを避けるように努力するってことだね。
実は、「Version: 2」みたいなカスタムHeaderをリクエストやレスポンスで使うこともできるんだよね。
Win32 APIにあるたくさんの「Ex」付き関数が良い例だね!まさにそれだよ!
/api/postsFinalFinalV2Copy1-2025(1)ExtraFixed
みたいなパスになっちゃうのは避けたいよね。
>結局、エンドポイントの名前に「/v2」が入ることはめったにない
これって本当に?一番使われてるHTTP API 100個調べてみたらどうなるか興味あるな。Dropbox APIは「v2」使ってるよ(URLでは「/2/」だけどね)。
v1からv2への移行ガイドも面白いね: https://www.dropbox.com/developers/reference/migration-guide
彼らがStoneっていう仕様言語を開発してるよ: https://github.com/dropbox/stone
grepで/*を検索して、/v2を無視すればいいってこと?
カーソルベースのページネーションには他にも便利な特徴があるよ。ユーザーがページを読み込んだ後で次のボタンを押した時、アイテムが追加されていても、インデックスベースだと前のページで見たアイテムが重複して表示されちゃうけど、カーソルベースなら見たことない新しいアイテムのリストが表示されるんだ。無限スクロールに便利だね。デメリットは、ページNにジャンプするボタンを作るのが難しいことかな。
カーソルは不透明にするべきだよ。データベースのサイズがバレないようにね。それに、不透明なカーソルには検索パラメータとか、ウォームキャッシュやルーティングのトポロジーなんかの追加の状態をエンコードすることもできるんだ。
全く同感!この件に関する最高の記事はこれだよ: https://use-the-index-luke.com/sql/partial-results/fetch-nex…
冪等性キーをRedisに保存するのは、全ての障害ケースで冪等性を保証できるとは思えないな。キーの保存と操作ペイロードは、アトミックにコミット(失敗したらロールバック)されるべきだよ。結局、冪等性キーってのは実行される操作(リクエスト)のIDそのものじゃないか?
アルゴリズムは、APIから200が返るか、ページがリフレッシュされるか、別のページに変わるたびにキーが変更されるってことだよ。
頼むから冪等性のために別のコンポーネントを追加するのはやめとけ。変な抽象化漏れとか、デリバリー保証を理解してないと壊れるぞ。ユーザーが既存データと一緒に進捗を追えるようなラベルやメタデータを書き込み時にサポートする方がずっと良いね。
もっとコメントを表示(2)
APIバージョニング、特にURLに入れるやつは、結局v1で止まっちゃうことが多いね。v2は珍しくて、v3はなぜか多い。クライアント側でURLを更新するのが面倒だからハードコードされがちだし、結局バージョンなんかめったに変わらないから使われないツールだよ。だから安全にスキップしても問題ないし、もし必要になったらv2のURLスペースを追加すれば良い。うちはAPIバージョンをクライアントヘッダーで管理して、古いバージョンは409で弾いてるよ。これならウェブアプリでは全く問題ないね。
Discord API[0]は今、バージョン6がデフォルトだけど、バージョン10ももう使えるみたいだよ。
https://discord.com/developers/docs/reference
例外もあるけど、Quickbooksはマイナーバージョン75までいってるらしいね。
バージョンなしのエンドポイントをv1のエンドポイントにフォールバックさせれば(例:/auth/loginと/v1/auth/loginを同じにする)、バージョニングもできるし、URLもシンプルにできるぞ。
ページネーションで、たった640Bとかのちっちゃいデータしか返さないのやめてくれ!AzureのBlobストレージなんて0Bのレスポンスもあったぞ。設計するときは、レコードの大きさを考えて、妥当な数のアイテムを1ページで返すようにしてくれ。あと、「30個のAPIエンドポイントがあるから、新しいバージョンごとに30個増える」って言うけど、変更する部分だけバージョンアップすれば良いんだよ。
ページサイズは、最初に考えてるよりも大きくすべきだね。ページネーションされてるエンドポイントは結局全部データを取得することが多いから、複数のリクエストによるオーバーヘッドは避けたい。ただ、最初にページネーションを実装しないと、後でデータ量が増えたときに破壊的変更になっちゃう。だから、大きいページサイズとページネーションを組み合わせるのが良いバランスだよ。
うん、ページネーションは良いオプションだし、デフォルトでも良いと思う。でも、それしか選択肢がないようにしないで、開発者がリクエスト数とペイロードサイズのトレードオフを選べるようにしてほしいね。
あのさ、ページネーションしか提供しないのって、バックエンドの都合があるの?全部のリソースをX回呼び出して取得するより、バックエンドの作業が少ないとか?
組み込み系の経験から言うと、メモリやレイテンシの制約が厳しいならページングは役に立つけど、ほとんどのAPIはそうじゃないでしょ。もちろん最大サイズは必要だけど、1200行のテキストを100行ずつしか返さず、オフにするオプションもないAPIには困るよね。
唯一同意できないのは、内部ユーザーもただのユーザーだってこと。技術的かもしれないけど、彼らも忙しいんだよ。自分のAPIを他の人に公開する前に、時間をかけて自分たちで使ってみるべきだね。一度公開しちゃったら、『ユーザー空間を壊さない』っていう契約を尊重しなきゃいけないんだから。
バージョン管理はまだこの問題を解決するのに役立つと思うよ。内部ユーザーに対しては、負担をかけないように色々できることがたくさんある。一番役立つのは、仕様について協力し合って、作業中のコピーを関係者に利用可能にすることだね。
内部ユーザーには、連絡して移行を促す手段があるから、APIのバージョンを廃止できるんだ。だからAPIバージョン管理は魅力的な解決策になるよ。僕もAPIバージョン管理に参加したし、それを便利だから使ってる組織も見てきたね。
大きな違いは、内部ユーザーには『アップデートしろ、さもないと』って言えることだよ。タダじゃないし、ちゃんとしたビジネス上の理由が必要だけど、内部組織があるから短期間でそれができるんだ。エンドユーザーや顧客には同じようにはできないよね、だって彼らは組織の一部じゃないんだから。
APIバージョン管理については少し違う意見もあるけど、議論はわかるな。べき等性については絶対に必須じゃないって意見には異論があるね。それは選択肢じゃなくて必須だよ。各リクエストにべき等性トークンを必須にする必要はないけど、指定できるオプションは必要だ。Stripe APIのクライアントが良い例で、自動でべき等性トークンを生成してくれるんだ。
このリストに足りないけど、いくつか大事だったこと:
1. Deadlines。リクエストが無意味になる期限を指定できるべき。APIの実装はその期限を使って保留中の操作をキャンセルできる。
2. 関連するけど、Backpressureと依存サービス。APIは自身の依存サービスを無駄なリトライで過負荷にしないように設計されるべき。いくつかのリトライは有用だけど、一般的にAPIはエラー状態を呼び出し元に素早く伝えるべきだね。
3. Static stability。APIの背後にあるシステムはStatic stabilityするように設計されるべき。そうすれば、変更操作が失敗しても何らかの機能は維持される。
確かにね。DELETE comments/32を3回連続で送った場合、3つのコメントが削除されるわけじゃない。最初の成功したリクエストでID 32のコメントが削除されて、残りのリクエストはもう削除されてて見つからないから404を返すって言うけど、そうとは限らないんだ。
多くの実装では、要素がそこに存在したかどうかに関わらず、DELETEが成功(つまり要素がなくなった)した場合、HTTP 204を返すんだよ。個人的には、404よりずっと理にかなってると思うけどな。
本当にそう。べき等性をアクションだけじゃなくて、可能な限りレスポンスにも広げるべきだね。
でも、なんで?それだとクライアントが得られる情報が厳密に減るだけじゃん。クライアントはDELETEに対する404をすでに処理できる能力を持ってるんだし。
「API」って聞くとみんなWeb APIを想像するけど、もともとは1940年代からある「Application Programming Interface」のことなんだよ。Web以前のAPIがどんな目的でどう使われてたか、今のWeb APIにどう関連するのか考えてみようぜ。昔のAPIは80年以上も前から存在してて、Web以前のAPIに関する本や論文は、この記事を読んでるみんなよりもずっと古いんだぜ。
あの頃のプログラマーはどうやって問題を解決してたんだろうね?
過去の話みたいに言ってるけど、WebじゃないAPIだって今でも山ほどあるよ。Every software library has an APIって感じ。Web系の奴らが「API」って言葉を「Web API」の略語みたいに使いまくってるの、マジでイラつくんだよね。
「Web系の奴らが『API』って言葉を『Web API』の略語みたいに使いまくってる」って言われても、俺はそうは思わないな。「API」って曖昧すぎて、一つのタイプのAPIだけを指すには不十分だよ。Webが登場する前も、Web APIも、修飾語がないと不完全。誰も用語を乗っ取ったわけじゃないし、君のその言い方も不完全だよ。
Web開発者は絶対に「API」を「Web API」の同義語として使ってるよね。だって、この記事のタイトルだって「良いAPI設計の全て」なのに、内容はWeb APIのことばっかりじゃん。普通ならAPI全般の話だと思うじゃん?
あなたはまるで昔の話みたいに言ってるけどさ、俺は20歳で、「API」って言われたらAPI/ABIのことしか考えないよ。プロトコルエンドポイントはAPIじゃないと思うな。
「1940年代から」ってマジ?!それはめちゃくちゃ早いじゃん。当時なんてサブルーチンライブラリすらほとんどなかったでしょ。その頃の「Application Programming Interface」の例が見てみたいな。
俺はMicrosoftが1990年代にWindowsで使い始めるまで、この言葉を聞いた記憶がないんだよね。それまではライブラリ関数とかスーパーバイザコールみたいな言い方だったな。まあ、あの頃は経験が少なかったから、もっと専門的なプログラマー用語は知らなかっただけかもしれないけどさ。
彼らはべき等性キーをRedisに保存することを提案してるけど、可能なら書き込みを行うシステムに、書き込み処理と単一トランザクションで一緒に保存すべきだと思うな。
「ユーザー空間を絶対に壊すな」っていう忠告はめちゃくちゃ重要なのに、よく見落とされがちだよね…Spotify、Reddit、Twitterとか、まさにそれじゃん。
技術的にダメなプロダクトだと、エレガントなAPIを作るのはほぼ無理ゲーだよね。API設計ってプロダクトの”基本的なリソース”(例えばJiraならissue、project、userとか)を反映するものだから、リソースが変だとAPIも変になるんだよな。不必要な抽象化されたリソースって、人間が直感的に理解しにくいし、特に初めて触る人には大変。それに、インシデント発生時のトラブルシューティングもマジでやりにくいんだよ。
これ最高!一つ付け加えるなら、APIの品質は、APIドキュメントの入手がどれだけ難しいかに反比例するってこと。契約しないとAPIドキュメントがもらえないなら、そのAPIは絶望的に悪いと決めつけていいよ。