こんにちは。ネットワークのパケットキャプチャを開きながらコーヒーをすする時間が、人生で一番落ち着くシニアインフラアーキテクトの私だ。
これまで幾百ものAPIトラブルや、深夜の障害対応をくぐり抜けてきた。その中で、意外と軽視されがちなのが「リソースの削除」を担当する DELETE メソッドの挙動だ。
「データを消すだけだから簡単だろう」と高をくくって設計した結果、二重送信(リトライ)で予期せぬエラーの嵐に見舞われたり、存在しないリソースへのリクエストでログが真っ赤に染まったりする……。そんな悲劇を、君たちには味わってほしくない。
今回は、REST APIにおける DELETE メソッドの正しい仕様(RFC 9110)、裏側のパケットの振る舞い、そして実務で即座に使える実装パターンまで、徹底的に解説しよう。
—
1. RFCが定義する DELETE メソッドの本質と「冪等性(Idempotency)」
Web APIの設計において、HTTPメソッドの意味論(セマンティクス)を正しく理解することは、堅牢なシステムを作るための第一歩だ。RFC 9110(HTTP Semantics)において、DELETE は次のように定義されている。
> 「DELETE メソッドは、オリジンサーバーに対して、ターゲットリソースによって識別されるリソースを削除するよう要求する。」
ここでエンジニアとして絶対に押さえておかながればならないのが、「冪等性(Idempotency)」 という性質だ。
冪等性とは何か?
冪等性とは、「同じ操作を何回繰り返しても、システムの状態が同じになる」という性質を指す。
DELETE メソッドは、この冪等性を持つように設計しなければならない。
- 1回目のリクエスト: サーバー上のリソース
DELETE /api/v1/users/123を削除する。結果:リソースは消え、ステータス200 OKまたは204 No Contentが返る。 - 2回目のリクエスト(ネットワークタイムアウト等の理由でクライアントが再送): すでにリソースは存在しないが、サーバー側としては「そのリソースはもう無い(削除完了状態)」というゴールは変わらない。
このとき、サーバーが 404 Not Found を返すのか、あるいは初回と同様に成功とみなすのかは設計判断が分かれるところだが、「クライアントが何度同じ DELETE リクエストを投げても、サーバー側のデータ構造が破壊されたり、意図しない別データが消えたりしないこと」が保証されている必要がある。これが DELETE の冪だ。
—
2. 成功時のステータスコード選定:200 OK か 204 No Content か
実務でよく議論になるのが、「削除成功時に何を返すべきか」という点だ。一般的に以下の2つのいずれかを選択する。
1. 204 No Content: レスポンスボディを返さない。リソースの削除に成功し、クライアントに伝えるべき追加データがない場合に最もよく使われる。ネットワーク帯域の無駄がなく、RESTful APIの王道と言える。
2. 200 OK: 削除されたリソースのメタデータや、「削除が完了しました」という確認用のJSONボディをあわせて返す場合に使う。
現場のアーキテクチャ方針にもよるが、純粋なリソース削除であれば 204 No Content をデフォルトの選択肢とするのが、パケット効率の観点からもスマートだ。
—
3. 実践:各種クライアントからの DELETE 実行コード
それでは、実務でよく使われる curl、Fetch API、そしてバックエンド(Python)からの DELETE リクエストの実装例を見ていこう。
① コマンドライン(curl)によるデバッグ
まずはネットワーク疎通確認やAPIの動作テストに欠かせない curl だ。-X DELETE を指定し、必要に応じて認証トークン(Bearer)を付与する。
# ユーザーID「123」を削除するリクエスト(レスポンスヘッダーも一緒に確認)
curl -X DELETE "https://api.example.com/v1/users/123" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1Ni..." \
-H "Accept: application/json" \
-i
*現場のTips*: -i オプションをつけることで、HTTPレスポンスのステータスコードやヘッダー(Content-Type や Date など)が標準出力に表示される。プロキシやAPI Gatewayの挙動を確認する際によく使うテクニックだ。
② モダンブラウザ / フロントエンド(JavaScript Fetch API)
ReactやVue.jsなどのフロントエンドからAPIを叩く際の実装例だ。
/**
* 指定したIDのユーザーを削除する非同期関数
* @param {string} userId - 削除するユーザーのUUID等
*/
async function deleteUser(userId) {
try {
const response = await fetch(`https://api.example.com/v1/users/${userId}`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + getAccessToken(),
'Content-Type': 'application/json'
}
});
// ステータスコードに応じたハンドリング
if (response.status === 204) {
console.log('削除が正常に完了しました(コンテンツなし)。');
return true;
} else if (response.status === 404) {
console.warn('対象のリソースはすでに存在しません。');
return false;
} else {
// 予期せぬエラー
const errorData = await response.json().catch(() => ({}));
throw new Error(`削除に失敗しました: ${response.status} ${errorData.message || ''}`);
}
} catch (error) {
console.error('通信エラーまたは予期せぬ例外が発生しました:', error);
// 必要に応じてリトライ処理やユーザーへの通知を行う
throw error;
}
}
③ バックエンド間連携(Python requests)
マイクロサービス間通信などで、Pythonから別サービスのAPIを叩いてリソースを掃除するシーンを想定したコードだ。
import requests
from requests.exceptions import RequestException
def remove_remote_resource(resource_id: str) -> bool:
"""
リモートAPIサーバーに対してDELETEリクエストを送信し、リソースを削除する
"""
url = f"https://internal-api.example.com/v1/resources/{resource_id}"
headers = {
"Authorization": "Bearer secret_service_token_xyz",
"X-Request-Source": "BatchWorker"
}
try:
# タイムアウトを必ず設定すること(無限ブロックの防止)
response = requests.delete(url, headers=headers, timeout=5.0)
# ステータスコードの評価 (200 OK または 204 No Content を成功とみなす)
if response.status_code in [200, 204]:
print(f"リソース {resource_id} の削除に成功しました。")
return True
elif response.status_code == 404:
# 冪等性を考慮し、すでに消えている場合は警告に留めて処理を継続させる
print(f"リソース {resource_id} は既に存在しません(404)。")
return True
else:
print(f"予期せぬステータスコードを受信しました: {response.status_code}, body: {response.text}")
return False
app_success = remove_remote_resource("res-987654")
—
4. 現場でハマりがちな「落とし穴」とトラブルシューティング
最後に、私が現場のコードレビューや障害対応で何度も目にしてきた「DELETE 設計のアンチパターン」をいくつか共有しよう。
トラブル1: リクエストボディ(body)にデータを詰め込んでしまう
HTTP仕様上、DELETE メソッドにリクエストボディを持たせることは不可能ではない(RFC上も禁止はされていない)。しかし、多くのロードバランサー、WAF(Web Application Firewall)、リバースプロキシ(NginxやEnvoyなど)は、DELETE リクエストのボディを破棄するか、転送しない仕様になっていることが多い。
- 対策: 削除対象の識別子は、必ずURIパス(例:
/users/{id})またはクエリパラメータに含めること。複雑な条件で複数の一括削除を行いたい場合は、DELETEではなくPOST /api/v1/users/batch-deleteのような専用のエンドポイント(トンネリング的アプローチ)を設計する方が安全だ。
トラブル2: 論理削除(Logical Delete)と物理削除(Physical Delete)の混同
データベースのレコードを本当に消す(物理削除)のか、それとも deleted_at カラムを更新するだけ(論理削除)にするのか。
APIの設計とバックエンドのストレージ実装が噛み合っていないと、クライアントは「消したつもり」なのに、一意制約(Unique Constraint)に引っかかって再作成時にエラーになるという現象が起きる。
- 対策: APIのインターフェースとして
DELETEを採用する場合、それが論理削除を意味するのか物理削除を意味するのかを、ドメインモデルの設計段階でチーム全体で明確に共有しておくこと。大半の業務システムでは、監査ログやデータ復旧の観点から「DELETEリクエストを受けたら内部で論理削除フラグを立てる」という実装が好まれる。
—
まとめ
DELETE メソッドは、ただデータを消すだけの地味な存在に見えて、実はWebアーキテクチャの美しさと堅牢性が最も試されるポイントの一つだ。
- 冪等性を意識し、何度リクエストしても安全な設計にすること
- 成功時は
204 No Contentを活用し、スマートなレスポンスを心がけること - プロキシやWAFの挙動を考慮し、ボディではなくURIでリソースを特定すること
これらを意識するだけで、君が設計するAPIの信頼性はワンランクもツーランクも跳ね上がる。さあ、今すぐ手元のコードを見直し、美しいRESTの原則をコードに落とし込もう。
コメント