Architecture
URLを変えずにREST APIをバージョン管理する:Spring 7での実践
URLに /v1 を入れる代わりにコンテントネゴシエーションを選んだ理由と、Spring Framework 7 なら3つのプロパティで実現できる方法を紹介します。
課題:APIを直接呼び出すクライアント
私は Spring Boot 4、つまり Spring Framework 7 で小さな個人用 REST API を運用しています。AIエージェント、スクリプト、いくつかの連携など、複数のクライアントがこのAPIを直接呼び出しています。/api/shorten や /api/random といったルートをそのまま叩いています。
どのエンドポイントもバージョン管理されていませんでした。何も変えなければ問題ありません。しかしレスポンス形式を変えたい日、たとえば単なる文字列ではなくJSONオブジェクトを返したくなった日には、すべてのクライアントが一度に壊れます。
そこで、バージョン1を壊さずにバージョン2をリリースする方法が必要でした。本当の問いは、クライアントが受け取るバージョンをどう選ぶか、です。
3つの選択肢
URLのプレフィックス(/api/v1/...)は最もよくある選択です。明示的でルーティングも簡単です。ただし、ルートごとにエイリアスを維持しない限り、既存の呼び出しはすべて壊れます。さらに、v1 と v2 のパッケージにコントローラーやDTOを複製する方向に流れがちです。
独自ヘッダー(X-API-Version)やクエリパラメータ(?version=1)ならURLはそのままです。こちらの方がましです。しかし、HTTPにすでに仕組みがあるのに、わざわざ新しい仕組みを作ることになります。
コンテントネゴシエーションは、まさにその標準の仕組みを使います。クライアントは Accept ヘッダーで特定の表現を要求します。
httpAccept: application/json;version=1
URLは変わりません。バージョンは要求するメディアタイプの一部であり、それこそが本来の役割です。
選んだ方法と、コストがほぼゼロの理由
コンテントネゴシエーションを選んだ理由はシンプルです。どのクライアントも移行する必要がないからです。バージョン指定のないリクエストは、これまで通りバージョン1を受け取ります。
うれしい驚きは Spring Framework 7 でした。APIのバージョン管理が標準で組み込まれています。インターセプターを書く必要も、パス定数を用意する必要もありません。application.properties に3行書くだけです。
propertiesspring.mvc.apiversion.supported=1.0 spring.mvc.apiversion.default=1.0 spring.mvc.apiversion.use.media-type-parameter[application/json]=version
1行目は受け付けるバージョンの一覧です。2行目は、クライアントが何も指定しない場合に1.0を返します。3行目は、application/json タイプの version パラメータからバージョンを読み取るよう Spring に指示します。
HTMLページ(Thymeleaf)は対象外です。このルールは application/json にのみ適用されます。
クライアント側から見た動き
クライアント側では、バージョン指定のないリクエストは通常どおり処理されます。
bashcurl -H "Accept: application/json" "https://api.example.com/api/random?min=1&max=6" # {"value":6,"min":1,"max":6,"timestamp":"..."}
未知のバージョンを指定すると、APIの他のエラーと同じ形式で400エラーが返ります。
bashcurl -H "Accept: application/json;version=9" "https://api.example.com/api/random?min=1&max=6" # {"status":400,"error":"Bad Request","message":"Invalid API version: '9.0.0'.","path":"/api/random"}
細かい点が2つあります。まず、406ではなく400です。Spring が InvalidApiVersionException を投げ、それをグローバルなエラーハンドラーが標準のレスポンスに変換しています。次に、Spring はバージョンを正規化します。メッセージの中では 9 が 9.0.0 になります。メッセージの文字列そのものをテストする前に知っておくと役立ちます。
バージョン2はこうなる
破壊的変更が必要になったら、同じコントローラー内で既存のメソッドの隣にメソッドを1つ追加します。
kotlin@GetMapping("/shorten") // version 1, implicite fun shorten(@RequestParam url: String): String = ... @GetMapping("/shorten", version = "2") // Accept: application/json;version=2 fun shortenV2(@RequestParam url: String): ShortenResponseV2 = ...
v2 パッケージも、コントローラーの複製も不要です。spring.mvc.apiversion.supported に 2.0 を追加するだけです。
テスト
@WebMvcTest で、JSONエンドポイントとHTMLページの両方の動作を確認します。必要なのは次の5ケースです。
- バージョン指定なし:バージョン1として処理される
version=1指定:処理される- 未知のバージョン(
version=9):400レスポンス - 不正な形式のバージョン(
version=abc):400レスポンス - HTMLページ:通常どおり応答する
kotlin@Test fun unsupportedVersionReturns400() { mockMvc.perform(get("/api/random").accept("application/json;version=9")) .andExpect(status().isBadRequest) .andExpect(jsonPath("$.status").value(400)) }
v2 を出す日までに残っていること
3つのテーマは意図的に後回しにしました。バージョンが1つしかない間は、どれも必要ないからです。
まずキャッシュです。私のAPIはCDNの背後にあります。同じURLに2つのバージョンがあると、共有キャッシュはレスポンスが Accept ヘッダーに依存することを知る必要があります。Spring は Vary: Accept を自動では付けないので、自分で追加する必要があります。
次に非推奨化です。Spring 7 は Deprecation、Sunset、Link ヘッダーでバージョンの終了を告知できます。バージョン1を廃止する日に役立ちます。
最後に OpenAPI Generator です。同じパスに複数のメディアタイプがあるケースの扱いが苦手です(プロジェクトの issue #11000)。今のところ仕様を公開していないので困ってはいませんが、公開する場合は注意が必要です。
振り返って
最小限のコストで、とても良い結果になりました。プロパティ3つ、移行なし、URLの重複なし。必要になる前からバージョン管理が整っていて、誰の邪魔にもなりません。
知っておくべきトレードオフが2つあります。バージョンがURLにもアクセスログにも出なくなるので、調査で必要なら明示的にログに出す必要があります。また、2つ目のバージョンができた時点で、キャッシュまわりには少し多めの注意が必要になります。
すでにクライアントがいるAPIで Spring Boot 4 を使っているなら、現時点で最もシンプルな選択だと私は考えています。検討して見送った選択肢も含めてプロジェクトのADRに記録してあるので、半年後に読み返しても判断の経緯が分かります。
// 次に読む
Business
Output-based vs Time & Material:AbbealがT&Mを葬った理由。
2026年、Abbealポートフォリオの78%がOutput-basedで稼働。粗利益+18pts、NPS+24、平均ミッション期間×1.7。運用方法と3つの成功条件。
11 min
Talent
アジア、欧州、北米にまたがるシニアエンジニアリングチームを構築する方法
アジア、欧州、北米の3大陸にまたがるシニアエンジニアリングチームを編成するプレイブック。Abbeal のトライハブモデル:パリ・モントリオール・東京。
7 min
IA
ClaudeでESNのCEOの1日を自動化した方法(そしてあなたがそこから得られるもの)。
Notion + BoondManager + Google Workspace + LinkedIn + Apollo + Calendly + Tactiqで30のワークフローをオーケストレーション、新しいSaaSなし。4つの柱:マルチチャネル重複防止セールス、48時間採用、インバウンドSEO/LinkedIn/AI引用、創業者の生産性。6ヶ月で失われたリードゼロ、以前の3〜4時間に対して1日15分。
7 min
IA
本番のAIエージェント:デモ劇場を避ける。
信頼性、コスト、セキュリティ、評価。クライアントで実際に使う7つのパターン。
9 min
GreenOps
GreenOps:クラウド請求を30%削減する7つのレバー。
パフォーマンスを犠牲にせず。具体例:請求-30%、SLO同等。
6 min
