はじめに:PUTの「全取替」という暴力と、PATCHの美学
ネットワークエンジニアとして数々のAPIトラブルや泥臭いパケット解析を乗り越えてくると、アプリケーション層のプロトコル設計がいかにインフラの帯域やDBの負荷に直結するかを痛感させられます。
Web APIを設計する際、リソースの更新処理であなたは何を使っていますか?思考停止で PUT メソッドを叩き込み、クライアントからリソースの「全体像」を毎回フルスクラッチで送信させていないでしょうか。
数メガバイトもあるユーザープロファイルのJSONや、ネストが深く巨大な設定ファイルの一部(例えば、通知設定のフラグ1つ)を変更するためだけに、わざわざリソース全体を送り直す。これはネットワーク帯域の無駄遣いであるだけでなく、データベースの行ロック競合や、最悪の場合はバリデーション漏れによる意図しないデータ消失(データの取りこぼし)を引き起こす温床となります。
今回は、RFC 5789で定義されるHTTPメソッド PATCH に焦点を当てます。リソースの「差分(Partial Update)」のみをエレガントに送信し、APIの通信量を最小限に抑えつつ、堅牢なデータ整合性を担保するための実践的な設計手法を、現場の知見を交えて徹底解説します。
—
1. PUTとPATCHの決定的な違い:RFC 5789が示す思想
まずは基本に立ち返りましょう。HTTP/1.1(RFC 7231)における PUT と、後から追加された PATCH(RFC 5789)の本質的な違いを整理します。
PUTの思想(置き換え / Replacement):- 指定されたURIにあるリソース「全体」を、クライアントから送られた表現で完全に置き換えます。
- もしリクエストボディに一部のフィールドしか含まれていない場合、厳密な仕様解釈としては「含まれていないフィールドは削除(あるいはデフォルト値に上書き)されるべき」とみなされます(※実装によりますが、これがPUTの本来のセマンティクスです)。
PATCHの思想(差分適用 / Partial Modification):- 指定されたURIのリソースに対して、一部の変更セット(差分)を適用します。
- リクエストに含まれていないフィールドは、既存のデータがそのまま維持されます。
なぜPATCHが必要なのか?
現場で最も恐ろしいのは 「上書きによるデータのロスト(Lost Update)」 です。
例えば、フロントエンドの別々のコンポーネントから、同じユーザーリソースに対して同時に更新がかかったとします。A画面からは「メールアドレスの変更」、B画面からは「アイコン画像の変更」を PUT で同時に投げた場合、ネットワークの遅延や処理順序によっては、先着した変更が後発のリクエストによって綺麗に吹き飛ばされてしまいます。
PATCH を正しく用いることで、「どのフィールドをどう変更したいのか」という意図だけをピンポイントでサーバーに伝え、アトミックな更新を実現できるのです。
—
2. PATCHのデータ表現:JSON Patch vs JSON Merge Patch
PATCH メソッドを実装する上で、エンジニアが必ず直面するのが「差分をどう表現するか」というフォーマットの問題です。代表的な2つの標準仕様を確認しておきましょう。
① JSON Merge Patch (RFC 7396)
最もシンプルで、直感的に使いやすい方式です。変更したいフィールドだけを持つJSONオブジェクトをそのまま送信します。値として null を指定すると、そのフィールドの削除を意味します。
- メリット: 実装が非常に容易。フロントエンドのコードも書きやすい。
- デメリット: 配列(Array)の一部分だけを更新する(例:「配列の3番目の要素だけ書き換える」)といった複雑な操作が表現できません。
② JSON Patch (RFC 6902)
操作命令(add, remove, replace, move, copy, test)のリストをJSON配列として記述する、より強力な方式です。
- メリット: 配列の操作や、条件付きの更新(
testオペレーションで現在の値が期待通りか検証してから適用するなど)が可能です。 - デメリット: ペイロードが冗長になりやすく、サーバー側のパーサー実装も少し複雑になります。
実務では、よほど複雑な配列操作が必要でない限り、シンプルで軽量な JSON Merge Patch (RFC 7396) から採用するのが無難です。Content-Typeには専用のメディアタイプである application/merge-patch+json を使用します。
—
3. 実践:通信フローとリクエスト・レスポンスの解剖
では、実際に PATCH リクエストがネットワーク上をどのように流れるのか、具体的な例を見てみましょう。
シーケンスのイメージ
[Client (Browser/App)] [API Gateway / Server]
| |
|--- HTTP PATCH /users/1001 ------------->|
| Content-Type: application/merge-patch|
| {"status": "active"} |
| |-- 1. 現在のDBレコード取得
| |-- 2. "status" のみを "active" に差分適用
| |-- 3. トランザクションCOMMIT
|<-- 200 OK (Updated Resource) -----------|
| {"id": 1001, "name": "Taro", |
| "status": "active", "updated_at": ..}|
HTTPリクエストの具体例(cURL)
ユーザーID 1001 のステータスだけを active に変更するリクエストを投げます。名前やメールアドレスは送信する必要がありません。
curl -X PATCH "https://api.example.com/v1/users/1001" \
-H "Authorization: Bearer eyJhbGciOi..." \
-H "Content-Type: application/merge-patch+json" \
-d '{"status": "active"}'
サーバー側の処理で気をつけるべき「冪等性(Idempotency)」の罠
HTTP仕様において、PUT は「何度実行しても結果が同じ(冪等)」であることが義務付けられていますが、PATCH はデフォルトでは冪等であるとは限りません(例えば「数値を1増やす」ような差分パッチの場合、実行するたびに値が増えてしまうため)。
実務のWeb API設計では、PATCH も事実上「冪等」として扱えるように設計するか、あるいは If-Match ヘッダー(Etagを用いた楽観的ロック)を組み合わせて、予期せぬ競合を防ぐのがシニアの流儀です。
—
4. コード実装例:Python (FastAPI) による堅牢なPATCHエンドポイント
現場のバックエンド開発でよく使われるPythonのモダンなフレームワーク FastAPI を用いて、JSON Merge Patchを受け付ける安全なエンドポイントの実装例を示します。
from datetime import datetime
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, EmailStr
from typing import Optional
app = FastAPI()
# データベース上のモックデータ
fake_users_db = {
1001: {
"id": 1001,
"name": "Taro Yamada",
"email": "taro@example.com",
"status": "pending",
"updated_at": "2023-10-01T00:00:00Z"
}
}
# 更新用のリクエストボディスキーマ
# すべてのフィールドをOptional(None許容)にすることで差分更新を表現する
class UserUpdateSchema(BaseModel):
name: Optional[str] = None
email: Optional[EmailStr] = None
status: Optional[str] = None
@app.patch("/v1/users/{user_id}", status_code=status.HTTP_200_OK)
def patch_user(user_id: int, payload: UserUpdateSchema):
# 1. リソースの存在確認
if user_id not in fake_users_db:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="指定されたユーザーが見つかりません。"
)
stored_user = fake_users_db[user_id]
# 2. 送信された(Noneではない)フィールドのみを抽出して差分適用
# exclude_unset=True を使うことで、クライアントが明示的に送信したキーだけを拾う
update_data = payload.dict(exclude_unset=True)
for key, value in update_data.items():
stored_user[key] = value
# 3. 更新日時の自動付与(インフラ/監査ログの観点からも重要)
stored_user["updated_at"] = datetime.utcnow().isoformat() + "Z"
# DBを更新(モック)
fake_users_db[user_id] = stored_user
return {
"message": "ユーザー情報を正常に部分更新しました。",
"data": stored_user
}
フロントエンド(JavaScript Fetch API)からの呼び出し例
クライアント側からJavaScriptで PATCH を投げる場合のコードスニペットです。
async function updateEmail(userId, newEmail) {
try {
const response = await fetch(`https://api.example.com/v1/users/${userId}`, {
method: 'PATCH',
headers: {
'Authorization': 'Bearer <your_token_here>',
'Content-Type': 'application/merge-patch+json',
},
// 変更したいフィールドだけに絞ったオブジェクトをJSON化する
body: JSON.stringify({
email: newEmail
})
});
if (!response.ok) {
throw new Error(`HTTPエラー! ステータス: ${response.status}`);
}
const result = await response.json();
console.log('更新成功:', result);
} catch (error) {
console.error('PATCHリクエストに失敗しました:', error);
}
}
—
5. インフラ・API設計の現場でハマりがちな罠とTips
最後に、ネットワークスペシャリストやインフラアーキテクトの視点から、現場でありがちなトラブルと回避のためのTipsをいくつか共有します。
① リバースプロキシ・WAFの制限に注意
NginxやAWS API Gateway、あるいはWAF(Web Application Firewall)の設定によっては、PUT や PATCH などのHTTPメソッドがデフォルトでブロックされていたり、リクエストボディのサイズ制限が厳しく設定されている場合があります。
「APIを作ったのになぜか405 Method Not Allowedや403 Forbiddenになる」という場合は、アプリケーションコードを見る前にインフラ層のルーティング設定を疑いましょう。
② Content-Type のバリデーションを厳格に
サーバー側で PATCH を実装する際は、リクエストヘッダーの Content-Type が application/merge-patch+json(または application/json-patch+json)であることを必ずチェック(バリデーション)してください。これを怠ると、クライアントが誤ったフォーマットでデータを送ってきた際に予期せぬパースエラーやデータ破損を引き起こす原因になります。
③ 部分更新の粒度をビジネスロジックに合わせる
あまりにも何でもかんでも PATCH 一発で書き換えられるように設計すると、ドメインモデルの整合性(インバリアント)が崩れることがあります。例えば「ステータスを active から suspended に変える時は、特定の理由コードが必須」といった複雑なビジネスルールがある場合は、汎用的な PATCH よりも、専用のエンドポイント(例: POST /v1/users/{id}/suspend)を切る方が、結果的に保守性の高い美しいAPIアーキテクチャになります。
—
おわりに
PATCH メソッドによる差分更新は、単なる「コードを少し楽にするテクニック」ではありません。ネットワーク帯域の最適化、データベースのロック競合の軽減、そしてシステム全体のスケーラビリティを支える極めて重要なアーキテクチャ上のピースです。
「とりあえず全部PUTで送っておけ」というエンジニアを卒業し、RFCの思想に基づいた洗練されたエンドポイント設計を手に入れたあなたのAPIは、確実に次のステージへ進化するはずです。
現場のインフラとアプリケーションの境界線を愛するエンジニアの皆様の、日々の設計・運用ライフの参考になれば幸いです。
コメント