Sunsetヘッダで旧APIを廃止予告する手順 — 移行ガイドの作り方まで
旧エンドポイントにDeprecationとSunsetのヘッダを付け、呼び出し側のコードをgrepして旧新の対応表と移行ガイドを作る手順を、RFC 9745とRFC 8594に沿って書き、Claude Codeのスキルにします。
旧APIを止めるときは、止める日をレスポンスのヘッダで先に伝え、移行ガイドへのリンクを同じレスポンスに載せます。使うのはDeprecationとSunsetの2つで、前者はRFC 9745、後者はRFC 8594が定義しています。この2つを混ぜて使うと、日付の書式を取り違える事故が起きます。
この記事では、2つのヘッダの役割の違い、旧エンドポイントへ付ける実装、呼び出し側のコードをgrepして旧新の対応表を作る手順、移行ガイドに書く項目までを順に書きます。後半では、同じ作業をClaude Codeのスキルにして繰り返せる形にします。互換性を壊す変更そのものの検出はClaude Codeでprotobufを変更する — buf breakingで後方互換を検証の領分で、ここでは壊すと決めた後の告知と移行文書を扱います。
DeprecationとSunsetは何を伝えるヘッダか
2つのヘッダは、API廃止の2つの段階に対応しています。RFC 8594は、廃止が2段階で進むことを前提に書かれています。1段階目は「推奨されなくなったが、まだ動く」状態で、2段階目は「APIやそのバージョンが応答しなくなる」状態です。Sunsetが適切なのは2段階目で、1段階目を表す用途には向かないと、RFC 8594自身が書いています。
1段階目を表すのが、後からRFC 9745として標準化されたDeprecationです。
| 項目 | Deprecation | Sunset |
|---|---|---|
| 定義 | DeprecationRFC 9745(Standards Track) | SunsetRFC 8594(Informational) |
| 伝える内容 | Deprecationリソースが非推奨になった(なる)日 | Sunsetリソースが応答しなくなると見込まれる日 |
| 値の書式 | DeprecationStructured Fieldsの日付(@+Unix秒) | SunsetHTTP-date |
| 過去の日付 | Deprecation許される(すでに非推奨) | Sunset過去は「今すぐ止まりうる」と解釈するのが安全 |
| 挙動の変化 | Deprecation伴わない | Sunset日付以降は4xx・3xx・接続不能などになりうる |
書式の差は実装でそのまま事故になります。RFC 9745の例では、2023年6月30日23:59:59 UTCの非推奨をDeprecation: @1688169599と書き、同じ文書が「歴史的な理由で、Sunsetは日付の書式が違う」と注記しています。2つを並べた例は次の形です。
Deprecation: @1688169599
Sunset: Sun, 30 Jun 2024 23:59:59 UTC2つを併用するときの決まりが1つあります。Sunsetの日付はDeprecationの日付より前であってはなりません(RFC 9745の4節)。非推奨になる前に止まる、という逆転は設定ミスとして扱われ、受け取った開発者は提供元に確認することになります。
もう1つ、Deprecationを付けても、リソースの挙動は変わりません。RFC 9745の5節は、ヘッダの存在は意味や機能の変更を示さず、利用者は従来どおり使い続けられると書いています。ヘッダを付けた時点で旧エンドポイントを遅くしたりエラーにしたりするのは、この定義から外れます。
旧エンドポイントにヘッダを付ける
実装は、旧バージョン配下のルートすべてにミドルウェアを1つ足すだけです。次はExpressでの例です。Expressの規定ではなく、RFCの書式に合わせて書いた実装例です。
import express from "express";
// 2026-12-31T23:59:59Z を Unix 秒にした値
const DEPRECATED_AT = 1798761599;
// HTTP-date は toUTCString() が出す書式に一致する
const SUNSET_AT = new Date("2027-06-30T23:59:59Z");
const GUIDE = "https://api.example.com/docs/migrate-v1-to-v2";
function markDeprecated(
_req: express.Request,
res: express.Response,
next: express.NextFunction,
) {
res.set("Deprecation", `@${DEPRECATED_AT}`);
res.set("Sunset", SUNSET_AT.toUTCString());
res.append("Link", `<${GUIDE}>; rel="deprecation"; type="text/html"`);
res.append("Link", `<${GUIDE}>; rel="sunset"; type="text/html"`);
next();
}
const app = express();
app.use("/v1", markDeprecated);SUNSET_AT.toUTCString()はWed, 30 Jun 2027 23:59:59 GMTという形で出力され、HTTP-dateの書式に一致します。Deprecation側は@に続けてUnix秒を書く点が違います。
ヘッダだけでは、利用者に「なぜ」と「どこへ移るか」が伝わりません。そこでLinkヘッダを併用します。RFC 9745はrel="deprecation"のリンクで非推奨に関する文書を指せるとしており、RFC 8594はrel="sunset"で廃止方針や緩和策の情報源を指せるとしています。移行ガイドのURLを両方に向けておけば、どちらの関係を探す利用者も同じ文書に着きます。
ここで、RFC 9745の例が示す使い分けも押さえておきます。Deprecationを付けずにLink: rel="deprecation"だけを返す形も許されていて、これは「まだ非推奨ではないが、非推奨にするときの方針はここにある」という意味です。廃止予告の前から方針文書を公開しておく運用ができます。
どのレスポンスにヘッダを付けるか
RFC 8594の5節によれば、Sunsetは返したリソース自身についての宣言です。API全体が止まる場合に、ホームのリソースだけに付けて全体に拡張して解釈させる設計も許されています。ただし拡張は、その規則を知らない利用者には見えません。旧バージョン配下のすべてのレスポンスに付けるほうが、確実に目に入ります。
呼び出し側のコードをgrepして対応表を作る
ヘッダは、読まれて初めて効きます。そこで、自分のリポジトリや社内の他リポジトリで旧APIを呼んでいる箇所を洗い出し、旧新の対応表を作ります。まず機械的に拾えるものから拾います。
# 旧パスを直接書いている箇所
git grep -n -E "/v1/(orders|users|invoices)" -- ':!docs'
# 旧パスを含むファイルの数を数える
git grep -l -E "/v1/" | wc -l
# 旧ベースURLを設定値として持っている箇所
git grep -n -E "API_BASE_URL|api\.example\.com/v1"grepは文字列しか拾えないので、パスを組み立てて呼ぶコード(${base}/${resource}のような形)は漏れます。拾えなかった箇所は、サーバーのアクセスログで旧パスへの実リクエストを集計して補います。ログに出る呼び出し元のクライアント名が分かれば、所有者への連絡先も決まります。
拾った結果は、次のような表にします。下は表の型を示すためのサンプルです。
| 旧リクエスト | 新リクエスト | 変更点 | 呼び出し箇所 |
|---|---|---|---|
GET /v1/orders/{id} | 新リクエストGET /v2/orders/{id} | 変更点レスポンスのtotalが文字列から数値に | 呼び出し箇所billing/client.ts:42 |
POST /v1/users | 新リクエストPOST /v2/users | 変更点nameがfirst_nameとlast_nameに分割 | 呼び出し箇所signup/api.ts:17 |
GET /v1/invoices | 新リクエストGET /v2/invoices?status=open | 変更点既定が全件から未払いのみに | 呼び出し箇所reports/export.ts:88 |
列は「旧・新・変更点・呼び出し箇所」の4つで足ります。変更点の列には、パスが変わるだけのものと、レスポンスの形や既定値が変わるものを分けて書きます。パスだけなら置換で済みますが、意味が変わるものは呼び出し側の改修が要るため、利用者が最初に見たい情報だからです。
Claude Codeに洗い出しを任せる
grepと表の穴埋めは、Claude Codeに任せやすい作業です。依頼文は、探す範囲と出力の形を固定するのがコツです。
/v1 配下のエンドポイントを呼んでいる箇所を git grep で洗い出して。
src/ と scripts/ が対象。docs/ は除く。
結果は「旧リクエスト / 新リクエスト / 変更点 / ファイル:行」の表にして、
新リクエストが openapi/v2.yaml から特定できないものは「要確認」と書いて。「要確認」を許す一文が肝心です。これがないと、新しいパスが見つからない行を推測で埋めてしまいます。出てきた表は、実際に1件ずつファイルを開いて確かめてからガイドに載せます。
移行ガイドに何を書くか
RFC 9745の3.1節は、非推奨に関する文書が「新しいリソースへ移るためのガイドと日程」を提供しうるとしています。RFC 8594の6節は、sunsetリンクの先に置ける情報を3つに分けています。
- 廃止の方針(どのリソースを、どれだけ前に予告して止めるか)
- 予定されている廃止の情報(いつ、どれが)
- 緩和策(代わりのリソースへ移る方法)
どちらも先の内容は自由で、人間向けでも機械向けでもよいとしています。この3つをそのまま見出しにすると、ガイドの骨格になります。
| 節 | 書く内容 | 出どころ |
|---|---|---|
| 日程 | 書く内容非推奨日、廃止日、廃止後の挙動 | 出どころミドルウェアの2つの定数 |
| 影響範囲 | 書く内容止まるパスの一覧 | 出どころgrepと実リクエスト集計 |
| 旧新の対応表 | 書く内容上の4列の表 | 出どころgrepの結果 |
| 破壊的な差分 | 書く内容型・既定値・認証の変更 | 出どころv1とv2のスキーマ差分 |
| 問い合わせ先 | 書く内容質問と延長の相談窓口 | 出どころ運用体制 |
廃止後の挙動は、必ず書いておく項目です。RFC 8594は、日付を過ぎた後に何が起きるかを4xx、3xxのリダイレクト、接続不能のいずれになりうるとしており、ヘッダからはどれになるか分からないと明記しています。利用者が実装を直すときに知りたいのはこの部分なので、ガイド側で「410を返す」のように1つに決めて書きます。
日付の書式は、ガイドとヘッダで揃えます。ガイドに「2027年6月30日」と書き、ヘッダがWed, 30 Jun 2027 23:59:59 GMTなら、日本時間では7月1日の朝になります。タイムゾーンを併記しておくと、利用者が日付を取り違えません。
手順をスキルにして、廃止のたびに回す
廃止は一度きりでは終わりません。v2の次にv3が出れば、同じ作業をもう一度やります。手順をスキルにしておくと、呼び出しだけで同じ流れが再現できます。スキルは.claude/skills/<スキル名>/SKILL.mdに置くとそのプロジェクトで読み込まれ、リポジトリにコミットすればチームにも配れます。
---
name: api-sunset-guide
description: 旧APIの呼び出し箇所を洗い出し、移行ガイドの下書きを作る
disable-model-invocation: true
allowed-tools: Bash(git grep *)
---
次の旧パスを呼んでいる箇所を洗い出す: $ARGUMENTS
呼び出し箇所の候補:
!`git grep -n -E "/v1/" -- ':!docs'`
上の候補を対象に、旧リクエスト・新リクエスト・変更点・ファイル:行の
4列の表を作る。新リクエストを特定できない行は「要確認」と書く。
表を作ってから、日程・影響範囲・破壊的な差分の見出しで下書きを書く。使うフロントマターの意味は次のとおりです。
disable-model-invocation: true: Claudeが自分の判断で呼び出せなくなり、人が/api-sunset-guideと打ったときだけ動きます。副作用のある作業やタイミングを自分で決めたい作業向けのフィールドですallowed-tools: スキルの実行中に確認なしで使えるツールを絞ります。ここではgit grepだけです$ARGUMENTS: スキル名の後ろに打った文字列に置き換わります。/api-sunset-guide /v1/ordersのように渡します!`コマンド`の行: スキルの内容がClaudeに渡る前にコマンドが実行され、行が出力で置き換わります
移行ガイドの実例はAgent SDK移行ガイド — Claude Code SDKからの破壊的変更のように、旧新の対応と破壊的な差分を軸に組んだ記事が参考になります。利用側でAPIの移行を自動化する話はClaude Codeのclaude-apiスキルでAPI移行を自動化するにあります。
動的コンテキストの!行は、Coworkセッションでは置き場所を示すプレースホルダに置き換わり、ローカル以外の一部のセッションでは文字のまま届きます。ローカルのターミナルで使う前提のスキルとして扱うのが安全です。
ヘッダを付けても利用者が気づかない場合の備え
ヘッダは通知手段の1つであって、すべてではありません。RFC 8594は、Sunsetは「ヒント」であり、クライアントはSunsetがなくても動くように実装されるべきだと書いています。日付が実際に守られる保証もないと、同じ文書にあります。RFC 9745も、非推奨に関する情報は任意であり、これに依存してはならないという立場です。次の点を押さえておくと、見落としを減らせます。
- ヘッダを読むクライアントは少ない前提で動く: SDKやHTTPクライアントが自動で警告を出すとは限りません。ガイドへの誘導は、ヘッダに加えてメールや更新履歴でも行います
- ヘッダの追加は挙動を変えない: 非推奨の段階では旧APIをこれまでどおり返し続けます。遅延やエラーで「気づかせる」運用はRFCの想定外です
- 機械可読な移行情報には認証が要る: RFC 8594は、リンク先が機械可読な移行規則を提供する場合、クライアントがそれに従って自動でURLを書き換えるなら、情報の正しさが重要になると注意しています。自動追従を許すなら、配布経路の信頼性を先に確保します
- キャッシュの有効期限とは別物:
Sunsetはリソースの状態ではなく、リソースが消える見込みの日付を示します。Cache-Controlの代わりにはなりません
最後に、ヘッダを付けたら実際に出ているかを確かめます。
curl -sI https://api.example.com/v1/orders/1 | grep -iE "^(deprecation|sunset|link):"3種類のヘッダが返れば、旧パスに掛けたミドルウェアが効いています。新パスのレスポンスには付いていないことも、あわせて見ておきます。
まとめ
廃止予告は、ヘッダ2つと文書1つの組です。Deprecationで非推奨の日、Sunsetで止まる日、Linkで移行ガイドの場所を同じレスポンスに載せ、日付の書式の違いと「SunsetをDeprecationより前にしない」だけ守れば、実装は数行で済みます。手間がかかるのは、呼び出し側の洗い出しと対応表のほうです。grepとアクセスログで拾い、拾えなかった行を「要確認」として残す運用にすれば、ガイドの精度は上がります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。