初めてのSaaSビルダー向け公開API設計:基本
初めてのSaaSビルダー向けに実用的にまとめた公開API設計:バージョニング、ページネーション、レート制限、ドキュメント、素早く出せる小さなSDKの選び方。

本当の問題:保守できるAPIを公開すること
公開APIは単にアプリが外に出すエンドポイントではありません。チーム外の人々に対して「この契約は変わっても動き続けます」という約束です。
難しいのは v1 を書くことではなく、バグ修正や機能追加、顧客の実際のニーズを学ぶ間に安定させておくことです。
初期の選択は後になってサポートチケットとして現れます。レスポンスの形が予告なく変わったり、命名が一貫していなかったり、クライアントがリクエストの成功を判別できないと、摩擦が生まれます。その摩擦は不信感に変わり、不信感は人々があなたの上に構築するのをやめさせます。
スピードも重要です。多くの初めてのSaaSビルダーはまず役に立つものを早く出し、その後改善します。トレードオフは単純です:ルールなしで早く出すほど、実際のユーザーが来たときにそれらの決定をやり直す時間が増えます。
v1 に対して「十分良い」状態は通常、実際のユーザー操作に対応する少数のエンドポイント、一貫した命名とレスポンス形、明確な変更戦略(たとえそれが "v1" だけでも)、予測可能なページネーションと合理的なレート制限、そして何を送れば何が返ってくるかを正確に示すドキュメントです。
具体例:ある顧客が夜間に請求書を作成する統合を構築したとします。後でフィールド名を変えたり、日付フォーマットを変更したり、部分的な結果を密かに返すようになると、彼らのジョブは午前2時に失敗します。彼らは自分のコードではなくあなたのAPIを責めるでしょう。
Koder.ai のようなチャット駆動のツールで多くのエンドポイントを素早く生成するのは魅力的ですが、公開する表面は小さく保ってください。内部用のエンドポイントを非公開にして学習期間中に何を長期的契約にするか決められます。
小さく、明確なAPI表面から始める
良い公開API設計は、顧客が製品について話す方法に合う少数の名詞(リソース)を選ぶことから始まります。内部データベースが変わってもリソース名は安定させてください。機能を追加するときは、コアリソースの名前を変えるよりフィールドを追加するか新しいエンドポイントを追加することを優先します。
多くのSaaS製品で実用的な出発点は:users、organizations、projects、events です。あるリソースを一文で説明できないなら、それは公開に向いていない可能性があります。
HTTPの使い方は退屈で予測可能に保ちましょう:
- GET はデータを読む(副作用なし)
- POST は何かを作る(またはアクションを開始する)
- PATCH はいくつかのフィールドを更新する
- DELETE は何かを削除または無効化する
認証は最初から凝らなくて構いません。APIが主にサーバー間(顧客がバックエンドから呼ぶ)であれば、APIキーで十分なことが多いです。個々のエンドユーザーとして動く必要がある場合や、ユーザーがアクセスを許可するサードパーティ統合を想定するなら OAuth の方が適切です。誰が呼び出すのか、誰のデータに触れるのかを平易な言葉で書いてください。
早い段階で期待値を設定してください。サポート対象とベストエフォートを明確に区別します。例えば:一覧エンドポイントは安定して後方互換性があるが、検索フィルタは拡張される可能性があり網羅的ではない、といった具合です。これによりサポートチケットを減らし、改善の自由度が保たれます。
Koder.aiのようなプラットフォーム上で構築するなら、APIをプロダクトとして扱い、まずは契約を小さくし、実際の使用に基づいて伸ばしてください。
コーナーに追い込まれないバージョニング
バージョニングは主に期待値の問題です。クライアントは「来週私の統合は壊れるのか?」を知りたい。あなたは改善の余地が欲しい。
URLバージョニングとヘッダーバージョニング
ヘッダーによるバージョニングは見た目がすっきりしますが、ログやキャッシュ、サポートのスクリーンショットに隠れやすいです。URLバージョニングは通常もっとシンプルです:/v1/...。顧客が失敗リクエストを送ったときバージョンがすぐ分かり、v1 と v2 を並行して動かすのも簡単です。
何が破壊的変更か、本当は?
正しく動作するクライアントがコードを変えずに動かなくなるなら、それは破壊的変更です。一般的な例:
- フィールド名の変更(例:
customer_idをcustomerIdにする) - フィールド型(文字列→数値)や意味の変更
- エンドポイントやレスポンスフィールドの削除
- バリデーションルールの厳格化(以前は任意だったものを必須にする)
- 認証要件やデフォルト権限の変更
安全な変更は古いクライアントが無視できるものです。新しいオプションのフィールドを追加するのは通常安全です。例:GET /v1/subscriptions のレスポンスに plan_name を追加しても、status しか読んでいないクライアントは壊れません。
実践的なルール:同じメジャーバージョンの中でフィールドを削除したり、再利用したりしないでください。新しいフィールドを追加し、古いものは残し、バージョン全体を廃止する準備ができたときに退役させます。
実行可能な廃止ポリシー
シンプルに保ちます:廃止を早めに告知し、レスポンスに分かりやすい警告を返し、終了日を設定します。最初のAPIでは90日間の猶予が現実的です。その間は v1 を動かし続け、短い移行ノートを公開し、サポートが一文で説明できるようにします:v1 はこの日まで動きます;v2 で何が変わったか、という具合です。
Koder.ai のようなプラットフォームで構築するなら、APIバージョンをスナップショットのように扱ってください:改善は新しいバージョンで出荷し、古いものは安定させ、顧客に移行の時間を与えた後に切り捨てます。
予測可能に保つページネーションパターン
ページネーションは信頼を勝ち取るか失うかの分かれ目です。リクエスト間で結果が跳んだりすると、人々はAPIを信用しなくなります。
データセットが小さくクエリが単純でユーザーがページ3を見に行くような場合は page/limit を使い、リストが大きく新しいアイテムが頻繁に入るかソートやフィルタが多い場合はカーソルベースを使います。カーソルは新しいレコードが追加されてもシーケンスを安定させます。
ページネーションを信頼できるものにするためのルール:
- 常にデフォルトのソートを定義する(例:
created_at desc)。 - 順序を決定するためのタイブレーカー(例:id)を追加する。
- ページネーションは契約の一部と見なす:後でソートを変えることは破壊的変更になる。
- 続行に必要な最小限を返す:items と次のカーソル(または次ページ)だけ。
Totals(総数)は扱いが難しいです。total_count はフィルタ付きの大きなテーブルでは高コストになりえます。安価に提供できるなら含め、できないなら省くかクエリフラグでオプションにしてください。
ここにシンプルなリクエスト/レスポンスの形を示します。
// Page/limit
GET /v1/invoices?page=2\u0026limit=25\u0026sort=created_at_desc
{
"items": [{"id":"inv_1"},{"id":"inv_2"}],
"page": 2,
"limit": 25,
"total_count": 142
}
// Cursor-based
GET /v1/invoices?limit=25\u0026cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wMS0wOVQxMDozMDowMFoiLCJpZCI6Imludl8xMDAifQ==
{
"items": [{"id":"inv_101"},{"id":"inv_102"}],
"next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wMS0wOVQxMDoyNTowMFoiLCJpZCI6Imludl8xMjUifQ=="
}
レート制限と親切なリトライ
レート制限は厳しくすることが目的ではなく、オンライン状態を保つことが目的です。トラフィックの急増からアプリを守り、高価なクエリを頻繁に投げるのを防ぎ、インフラ費用の驚きを避けます。制限は契約でもあり、クライアントは通常の使用量がどれくらいかを知れます。
まずはシンプルに始めて後で調整します。典型的な使用をカバーし短いバーストに余裕を持たせるものを選び、実際のトラフィックを観察してください。データがなければ安全なデフォルトは API キーごとに 60 requests/minute と小さなバースト許容です。あるエンドポイントが重い(検索やエクスポートなど)なら、すべてのリクエストを罰する代わりに、そのエンドポイントに厳しい制限や別のコストルールを設けてください。
制限を適用するときは、クライアントが正しい行動を取りやすくします。429 Too Many Requests を返し、いくつかの標準ヘッダーを含めてください:
X-RateLimit-Limit: ウィンドウ内で許可される最大X-RateLimit-Remaining: 残り回数X-RateLimit-Reset: ウィンドウがリセットされる時刻(タイムスタンプか秒数)Retry-After: 再試行まで待つ時間
クライアントは 429 を通常の状態として扱うべきで、戦うエラーと考えるべきではありません。穏やかなリトライパターンは両者を幸せにします:
Retry-Afterがある場合はそれを待つ- そうでなければ指数バックオフを使う(例:1s、2s、4s)
- 多くのクライアントが同時に再試行しないよう少しランダム性(ジッター)を加える
- 待ち時間の上限を設ける(例:30–60秒)
例:顧客が夜間の同期でAPIを激しく叩く場合、ジョブは1分間にリクエストを分散し、429 を受けたら自動的に遅くすることで全体が失敗するのを防げます。
エラー、ステータスコード、セーフな書き込み
エラーが読みにくいとサポートチケットはすぐに山になります。1つのエラー形に統一し、500系でも同じ形を使ってください。シンプルな標準は:code、message、details、そしてサポートに貼れる request_id です。
小さく予測可能なフォーマットの例:
{
"error": {
"code": "validation_error",
"message": "Some fields are invalid.",
"details": {
"fields": [
{"name": "email", "issue": "must be a valid email"},
{"name": "plan", "issue": "must be one of: free, pro, business"}
]
},
"request_id": "req_01HT..."
}
}
HTTPステータスコードはいつも同じ意味で使ってください:入力不正は400、認証がないまたは無効は401、認可がないのは403、リソースが見つからないのは404、競合(重複や状態不整合)は409、レート制限は429、サーバーエラーは500。整合性は工夫より重要です。
バリデーションエラーは直しやすくしてください。フィールドレベルのヒントは内部DB列名ではなくドキュメントで使う正確なパラメータ名を指すべきです。形式要件(日付、通貨、列挙型)があるなら受け付ける形式を示し、例を出してください。
リトライは多くのAPIが誤って重複データを作るポイントです。重要な POST アクション(支払い、請求書作成、メール送信など)には冪等キーをサポートして安全に再試行できるようにします。
- 選択した POST エンドポイントで
Idempotency-Keyヘッダーを受け付ける。 - 結果とともにキーを短期間(例:24時間)保存する。
- 同じキーの再リクエストには最初のレスポンスと同じものを返す。
- 最初のリクエストがまだ処理中なら、2つ目のリソースを作らずに再試行してほしい旨を返す。
この一つのヘッダーでネットワークが不安定な場合やタイムアウトが起きた場合の多くの厄介な境界条件が防げます。
例:小さなSaaS請求APIの実践
あなたが projects、users、invoices の3つの主要オブジェクトを持つシンプルなSaaSを運営しているとします。プロジェクトには多くのユーザーがいて、各プロジェクトには月次の請求書が発生します。クライアントは請求書を会計ツールに同期したり、自分のアプリ内で基本的な請求情報を表示したりしたい。
クリーンな v1 は例えば以下のようになります:
GET /v1/projects/{project_id}
GET /v1/projects/{project_id}/invoices
POST /v1/projects/{project_id}/invoices
ここで破壊的変更が起きるとします。v1 では請求額をセント単位の整数で保存していた:amount_cents: 1299。後にマルチカレンシーや小数が必要になり、amount: "12.99" と currency: "USD" にしたいとします。古いフィールドを上書きしてしまうと既存の統合はすべて壊れます。バージョニングがあれば慌てる必要はありません:v1 は安定させ、/v2/... を出して新フィールドを導入し、クライアントが移行するまで両方をサポートします。
請求書一覧には予測可能なページネーション形を使います。例えば:
GET /v1/projects/p_123/invoices?limit=50\u0026cursor=eyJpZCI6Imludl85OTkifQ==
200 OK
{
"data": [ {"id":"inv_1001"}, {"id":"inv_1000"} ],
"next_cursor": "eyJpZCI6Imludl8xMDAwIn0="
}
ある日、顧客がループで請求書をインポートしてレート制限に引っかかりました。ランダムな失敗ではなく、彼らは明確なレスポンスを受け取ります:
429 Too Many RequestsRetry-After: 20- 小さなエラーボディ
{ "error": { "code": "rate_limited" } }
クライアント側は20秒待ってから同じ cursor で続行でき、すべてを再ダウンロードしたり重複請求書を作る必要はありません。
ステップ・バイ・ステップ:v1 API の簡単なリリース計画
v1 のローンチはエンドポイントの束ではなく小さなプロダクトリリースとして扱うと上手くいきます。目標はシンプル:人々がそれに構築でき、あなたは驚きなく改善を続けられることです。
実用的な1週間プラン(小さなチームでも)
まずは API の目的と非目的を説明する1ページを書きます。表面積を一言で説明できるくらい小さくしてください。
順序は以下の通り。各ステップが十分でない限り次に進まないでください:
- コアリソースと最初にサポートするエンドポイント(5–10 個)を名前付けした単一ページの仕様を作る。ベースURLパターン、認証方法、必要なヘッダーを含める。
- 各エンドポイントについて、現実的なリクエストとレスポンスを1つずつ書く。これらの例は最初のドキュメントと最初のテストになります。
- 短いルールセクションを追加する:エラーの形、ページネーションの方法(一覧がある場合)、レート制限、安定するフィールドと変更されうるフィールド。
- 偽のクライアントで内部テストを行う。顧客になりきって新しいリポジトリで統合を構築するふりをする。最初の成功コールまでどれくらいかかるか時間を計り、困った点をメモする。
- v1 契約を公開する:安全な変更(加法的フィールド)、新バージョンが必要な変更、破壊的変更の通知期間。
コード生成ワークフロー(例:Koder.ai を使ってエンドポイントとレスポンスをスキャフォールドする)を使う場合でも、偽クライアントテストはやってください。生成コードは見た目は正しくても使いにくいことがあります。
その対価はサポートメールの減少、ホットフィックスの減少、そして実際に保守できる v1 です。
小さなSDKを過剰設計せずに出荷する
最初のSDKは第二のプロダクトではありません。HTTP API の薄いフレンドリーなラッパーだと考えてください。一般的な呼び出しを簡単にする一方で、APIの仕組みを隠しすぎないこと。まだラップしていない機能が必要なら、生のリクエストに戻れることが重要です。
顧客が実際に使っている言語に基づいて1つの言語を選んでください。多くのB2B SaaS APIでは JavaScript/TypeScript か Python が一般的です。1つのしっかりした SDK を出す方が、3つの中途半端なものを出すより価値があります。
小さなSDKに含めるべき内容
初期に良いセットは:
- 認証処理(APIキーやOAuthトークン)を一箇所にまとめる
- 妥当なタイムアウトと安全なリクエスト(GET)の自動リトライ、バックオフ
- イテレータや次ページ関数を返すページネーションヘルパー
- わかりやすいリクエスト/レスポンスモデル(基本的な型で良い)
- カスタムヘッダーや生のHTTP呼び出しのためのエスケープハッチ
手作りでも OpenAPI から生成してもよいです。生成は正確な仕様があり型の一貫性が欲しいときに優れていますが、多くのコードを生み出すことが多いです。初期段階では手書きの最小クライアントに OpenAPI ファイルでドキュメントを添えるだけで十分なことが多いです。公開 SDK インターフェースが安定していれば、後で生成クライアントに切り替えてもユーザーを壊さずにできます。
SDK と API のバージョンを分ける
API バージョンは互換性ルールに従い、SDK バージョンはパッケージングルールに従います。
新しいオプションパラメータやエンドポイントを追加するのは通常マイナーな SDK バンプで良いです。メソッド名の変更やデフォルトの変更など SDK 自体の破壊的変更はメジャーリリースにしてください。これによりアップグレードが穏やかになりサポートチケットが減ります。
サポート地獄を招く一般的なミス
ほとんどのAPIサポートチケットはバグではなく驚きについてです。公開API設計の本質は退屈で予測可能であることです。クライアントコードが月ごとに問題なく動き続けることを目指します。
信頼を失う一番の早道は、誰にも言わずにレスポンスを変えることです。フィールド名を変える、型を変える、値の代わりに null を返すようになる――これらはクライアントを壊します。どうしても振る舞いを変える必要があるなら、バージョンを切るか新しいフィールドを追加してしばらく古いものを残し、明確なサンセット計画を示してください。
ページネーションも繰り返し問題になります。一つのエンドポイントが page/pageSize、別のエンドポイントが offset/limit、別のがカーソルでそれぞれデフォルトが違うと問題が起きます。v1 では一つのパターンを選んで全体に適用してください。ソートも安定させて、次ページでアイテムが飛んだり重複したりしないようにします。
エラーの不一致も多くの往復を生みます。あるサービスは { "error":"..." } を返し、別のサービスは { "message":"..." } を返すとクライアントはエンドポイントごとに特別なハンドラを作る羽目になります。
以下は最も長いメールスレッドを生む5つのミスです:
- フィールドの静かな変更(名前、型、意味)をバージョンアップや廃止ウィンドウなしで行う
- エンドポイントごとに異なるページネーションルール、またはデフォルトが時間と共に変わる
- エラー形式、ステータスコード、バリデーションメッセージがエンドポイントでばらばら
- request_id がないため失敗した呼び出しをログで特定できない
- 突然発生するレート制限でヘッダーや再試行ガイダンスがなく、バックオフ例もない
簡単な習慣が役立ちます:すべてのレスポンスに request_id を含め、すべての 429 に再試行の説明を入れること。
クイックチェックリストと次のステップ
公開前に一貫性に焦点を当てた最終チェックを行ってください。ほとんどのサポートは小さな詳細の不一致から起きます。
よく問題を見つけるチェック:
- 命名:名詞の一貫性、複数形の扱い、フィールドのケーシング(どれか一つを選んで守る)
- 例:すべてのエンドポイントに現実的なリクエストとレスポンスの例があること(ページネーション含む)
- エラー:明確なエラー形、安定したエラーコード、よくある失敗に対する有用なメッセージ
- 制限:レート制限の振る舞いが文書化され、レスポンスに期待するヘッダーが含まれていること
- 安全性:作成系の再試行に対する冪等性、タイムアウトの予測可能な扱い
ローンチ後は、期待ではなく実際に人々が叩いているものを監視してください。最初は小さなダッシュボードと週次レビューで十分です。
まず監視すべき信号:
- 429 スパイク(誰が制限を受けているか、なぜか)
- エンドポイントごとの p95 レイテンシ(遅いエンドポイントは N+1 クエリを隠していることが多い)
- 上位のエンドポイントとパラメータ(実際のAPI表面)
- ステータスコード別のエラー率(400 と 401 と 500 など)
- タイムアウトとリトライ(クライアントが無限ループしている可能性)
フィードバックを集めてもすべてを書き換えないでください。ドキュメントに問題報告の短い経路を用意し、各報告にエンドポイント、request id、クライアントバージョンをタグ付けしてください。修正する際は加法的な変更(新しいフィールド、新しいオプションパラメータ、新エンドポイント)を優先し、既存の振る舞いを壊すのは避けます。
次のステップ:あなたのリソース、バージョニング計画、ページネーションルール、エラー形式をまとめた1ページのAPI仕様を書きます。その後ドキュメントと認証+2〜3コアエンドポイントをカバーする小さなスターターSDKを作ります。もっと速く進めたいなら、チャットベースの計画(Koder.ai のようなツールのプランニングモード)で仕様、ドキュメント、スターターSDK を下書きしてから生成しても良いでしょう。
よくある質問
How many endpoints should I expose in my first public API?
Start with 5–10 endpoints that map to real customer actions.
A good rule: if you can’t explain a resource in one sentence (what it is, who owns it, how it’s used), keep it private until you learn more from usage.
What resources should I start with for a SaaS API?
Pick a small set of stable nouns (resources) customers already use in conversation, and keep those names stable even if your database changes.
Common starters for SaaS are users, organizations, projects, and events—then add more only when there’s clear demand.
Which HTTP methods should I use, and why does it matter?
Use the standard meanings and be consistent:
GET= read (no side effects)POST= create or start an actionPATCH= partial updateDELETE= remove or disable
The main win is predictability: clients shouldn’t guess what a method does.
Should I version my API in the URL or in headers?
Default to URL versioning like /v1/....
It’s easier to see in logs and screenshots, easier to debug with customers, and simpler to run v1 and v2 side by side when you need a breaking change.
What counts as a breaking change in an API?
A change is breaking if a correct client can fail without changing their code. Common examples:
- Renaming fields
- Changing field types or meaning
- Removing fields or endpoints
- Making previously-optional input required
- Changing auth rules or default permissions
Adding a new optional field is usually safe.
What’s a realistic deprecation policy for a v1 API?
Keep it simple:
- Announce it early
- Include a clear warning in responses (and/or docs)
- Set a firm end date
A practical default is a 90-day window for a first API, so customers have time to migrate without panic.
Should I use page-based or cursor-based pagination?
Pick one pattern and stick to it across all list endpoints.
- Use page/limit when datasets are small and people jump to page 3.
- Use cursor when lists grow large or change often.
Always define a default sort and a tie-breaker (like created_at + id) so results don’t jump around.
What rate limit should I set, and what should I return on 429?
Start with a clear per-key limit (for example 60 requests/minute plus a small burst), then adjust based on real traffic.
When limiting, return 429 and include:
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-ResetRetry-After
This makes retries predictable and reduces support tickets.
What should a good API error response look like?
Use one error format everywhere (including 500s). A practical shape is:
code(stable identifier)message(human-readable)details(field-level issues)request_id(for support)
Also keep status codes consistent (400/401/403/404/409/429/500) so clients can handle errors cleanly.
How do I ship docs and a small SDK without over-engineering it?
If you generate lots of endpoints quickly (for example with Koder.ai), keep the public surface small and treat it as a long-term contract.
Do this before launch:
- Write one realistic request + response example per endpoint
- Lock in pagination and error shapes
- Add idempotency keys for important
POSTactions - Build a “fake client” internally to see where it’s confusing
Then publish a tiny SDK that helps with auth, timeouts, retries for safe requests, and pagination—without hiding how the HTTP API works.