ZPL-0485 Python開発 実測 24/25
Python HTTP APIのエンドポイントとエラー・権限を設計するPrompt
API設計を「考え始める」状態から「次の行動が決まる」状態へ。
操作・所有関係・入力制約から、成功/失敗レスポンスと他人ID検査をそろえます。
このPromptでできること
- 利用者とresource、所有権、操作、フィールド制約を入力すると、endpoint契約、status/error matrix、権限/他人IDのテストを作り、実装前の曖昧さを露出します。
必要なもの(入力項目)
- API利用者/認証方式・役割
- resourceと所有者/組織の関係
- 必要操作と状態遷移
- path/query/bodyの実フィールド・validation
- 返してよいresponse fields
- 既存APIのstatus/error形式
- 再試行・idempotency方針
- 使用framework/versionと公開範囲
解決できる悩み
- API設計で何を決めるべきか分からない
- 一般論しか出ず、自分のケースに使えない
- 重要な前提やリスクを見落とす
- 結果を実行可能な形へ落とし込めない
こんな人向け
- Pythonプログラマーの初心者
- 実務を時短したい担当者
- AIの出力を判断材料として使いたい人
使うタイミング
API設計に着手する直前、既存案を見直すとき、または公開・実行前に抜け漏れを確認するとき。
プロンプト本文
{{ }} の部分を自分の情報に置き換えてから使ってください。
Python製HTTP APIを実装する前に、endpointごとの正常・失敗・権限契約を作ってください。コードは契約の後です。
利用者、認証方式、役割:{{利用者・認証}}
resourceと所有者/組織の関係:{{resource・所有関係}}
必要操作と状態遷移:{{操作・状態遷移}}
path/query/bodyの実フィールドと検証制約:{{request fields・検証}}
レスポンスに公開可能なフィールドと秘密:{{response fields・秘匿}}
既存APIのstatus/error body方針:{{既存status・error}}
再試行・idempotencyの要件:{{再試行方針}}
framework/versionと公開範囲:{{framework・公開範囲}}
resourceごとにmethod/path、request schema、success status/body、error status/body、必要roleと所有権チェックを表にする。認証済みでも他人のobject IDを与えた場合の試験を必ず含める。validation、未認証、権限不足、存在しないID、状態競合のどれを返すかは既存契約と秘匿方針を優先し、曖昧なら候補と確認質問を出す。FastAPIを使う場合のみPydantic/HTTPException/response modelの実装例を任意追加。RFC9457のproblem+jsonは採用時だけ適用し、内部traceや秘密情報をerror bodyへ出さない。GETに不必要なbodyを設計しない。
出力:endpoint契約表、正常/失敗JSONの短い例、error matrix、認証/所有権/validationテスト、未確定事項。HTTP statusとbodyの矛盾を検査する。
使い方
返ってくるもの
- method/pathとschema、success/error matrix、JSON例、role/所有権チェック、テスト
調整のしかた
「目的・利用者」を変更すると対象が変わり、「完成イメージ・成功指標」を具体化すると提案精度が上がります。
追加で投げると効く指示
- 初心者向けに手順を細かくしてください
- 上位3案を比較表にしてください
- 不足情報だけ質問してください
- リスクの高い順に並べてください
推奨モデル
コーディング能力・高推論モデル推奨
実測の入力と出力
下は実際にAIへ通したときの入力と、そのとき返ってきた出力です(モデル: gpt-5.1-thinking / 2026-09-16)。作例ではありません。
このとき入力した内容を見る
Python製HTTP APIを実装する前に、endpointごとの正常・失敗・権限契約を作ってください。コードは契約の後です。
利用者、認証方式、役割:社内の業務アプリ(Web フロント)から使う。認証は社内 SSO で発行された JWT を Authorization ヘッダで受け取る。役割は member と admin の2つ。
resourceと所有者/組織の関係:resource は「経費申請(expense)」。申請は作成した社員(owner)のもの。admin は同じ組織(org_id)の全申請を見られる。
必要操作と状態遷移:作成(draft)→ 申請(submitted)→ 承認(approved)または差し戻し(returned)。returned は owner が編集して再申請できる。approved 後は編集・削除不可。承認・差し戻しは admin のみ。
path/query/bodyの実フィールドと検証制約:作成:amount(整数・1〜1,000,000)、category(travel / meal / other)、memo(0〜200文字)、receipt_url(任意・https のみ)。一覧:status と month(YYYY-MM)で絞り込み。
レスポンスに公開可能なフィールドと秘密:返してよい:id, amount, category, memo, status, created_at, owner_name。返さない:owner の社員番号、他の社員の申請の存在。
既存APIのstatus/error body方針:既存の社内API は、エラー時に {"error": {"code": "...", "message": "..."}} を返している。404 と 403 の使い分けは決まっていない。
再試行・idempotencyの要件:フロントが通信エラーで作成を再送することがある。二重作成は避けたい。
framework/versionと公開範囲:FastAPI 0.110 / Python 3.12。社内ネットワークのみに公開。
resourceごとにmethod/path、request schema、success status/body、error status/body、必要roleと所有権チェックを表にする。認証済みでも他人のobject IDを与えた場合の試験を必ず含める。validation、未認証、権限不足、存在しないID、状態競合のどれを返すかは既存契約と秘匿方針を優先し、曖昧なら候補と確認質問を出す。FastAPIを使う場合のみPydantic/HTTPException/response modelの実装例を任意追加。RFC9457のproblem+jsonは採用時だけ適用し、内部traceや秘密情報をerror bodyへ出さない。GETに不必要なbodyを設計しない。
出力:endpoint契約表、正常/失敗JSONの短い例、error matrix、認証/所有権/validationテスト、未確定事項。HTTP statusとbodyの矛盾を検査する。
返ってきた出力を見る
以下を「実装前のAPI契約 v0.1」とします。今回は契約を固める段階なので、FastAPI/Pydanticコードは入れていません。
1. 契約上の基本原則
認証済みでも、member が他人の expense_id を指定した場合は 403 ではなく 404 とします。同様に、admin が別 org_id の申請IDを指定した場合も 404 とします。これにより、権限のない利用者からは「そのIDの申請が存在するか」を判別できません。
一方、利用者がその申請を見る権限は持っているものの、操作だけが許可されていない場合は 403 を返します。たとえば同一組織の admin が他人の申請を PATCH しようとした場合です。
エラー形式は既存APIに合わせて統一します。
JSON
{
"error": {
"code": "EXPENSE_NOT_FOUND",
"message": "Expense not found."
}
}
内部例外、SQL、JWT claims、社員番号、stack trace、実際の所有者、実際の現在状態などはエラーbodyへ出しません。
2. Expense response schema
全endpointで公開可能なExpense表現を統一します。
JSON
{
"id": "exp_123",
"amount": 12800,
"category": "travel",
"memo": "東京出張",
"status": "draft",
"created_at": "2026-09-16T06:30:00+09:00",
"owner_name": "山田 太郎"
}
返さないフィールド:
社員番号
org_id
JWT subject
内部DB ID
receipt_url
その他内部監査情報
receipt_url は入力可能ですが、今回提示された「返してよいフィールド」に含まれていないためresponseには含めません。
3. Endpoint契約表
Method / Path Request Success 必要role / 所有権 主な失敗
POST /expenses JSON: amount, category, memo, receipt_url?。Idempotency-Key header必須 201 Created + Expense member または admin。ownerはJWTから決定 401, 409, 422
GET /expenses Query: status?, month?。bodyなし 200 OK + {"items":[...]} member=自分のみ。admin=同一org全部 401, 422
GET /expenses/{expense_id} bodyなし 200 OK + Expense owner、または同一org admin 401, 404
PATCH /expenses/{expense_id} 更新するフィールドだけ送信 200 OK + Expense ownerのみ。draft/returned のみ 401, 403/404, 409, 422
DELETE /expenses/{expense_id} bodyなし 204 No Content ownerのみ。暫定で draft/returned のみ 401, 403/404, 409
POST /expenses/{expense_id}/submit bodyなし 200 OK + Expense ownerのみ。draft/returned → submitted 401, 403/404, 409
POST /expenses/{expense_id}/approve bodyなし 200 OK + Expense 同一org adminのみ。submitted → approved 401, 403/404, 409
POST /expenses/{expense_id}/return bodyなし 200 OK + Expense 同一org adminのみ。submitted → returned 401, 403/404, 409
PATCH status=... のように状態を直接書き換えさせず、submit / approve / return を別操作にしています。これにより「誰がどの遷移を行えるか」をendpoint契約として固定できます。
4. Request schema
POST /expenses
field 型 必須 制約
amount integer yes 1 <= amount <= 1,000,000。boolは不可
category string enum yes travel, meal, other
memo string yes 0〜200文字
receipt_url string no HTTPS URLのみ
owner、owner_name、org_id、status、created_at はクライアントから受け取りません。
status は常にサーバ側で draft にします。
PATCH /expenses/{id}
JSON
{
"amount": 15000,
"memo": "東京出張・交通費"
}
更新可能:
amount, category, memo, receipt_url
id, owner, org_id, status, created_at は変更不可です。
少なくとも1フィールド必要とし、空 {} は 422 とするのを推奨します。
GET /expenses
GET /expenses?status=submitted&month=2026-09
status:
draft | submitted | approved | returned
month:
YYYY-MM
両方指定された場合は AND 条件です。
GETにrequest bodyは持たせません。
5. State transition契約
現状態 edit delete submit approve return
draft owner ○ owner ○ owner ○ × ×
submitted × ×* × admin ○ admin ○
returned owner ○ owner ○ owner ○ × ×
approved × × × × ×
submitted のDELETEについては、今回「withdraw/cancel」という状態遷移が定義されていないため、暫定で削除不可としています。
したがって、
http
PATCH approved expense
も、
http
DELETE approved expense
も 409 Conflict です。
これは権限不足ではなく「そのresourceの現在状態と操作が競合している」ためです。
6. Idempotency契約
作成の二重登録防止には、resource内容からの推測的な重複判定ではなく Idempotency-Key を使うのが安全です。
http
POST /expenses
Authorization: Bearer ...
Idempotency-Key: 19db1826-d1cf-4a68-aed5-...
同じ認証主体・同じendpointで、
同じkey + 同じrequest内容
なら、最初に作った同じexpenseを返します。新規行を作りません。
http
201 Created
JSON
{
"id": "exp_123",
"amount": 12800,
"category": "travel",
"memo": "東京出張",
"status": "draft",
"created_at": "2026-09-16T06:30:00+09:00",
"owner_name": "山田 太郎"
}
同じkey + 異なるrequest内容
なら、
http
409 Conflict
JSON
{
"error": {
"code": "IDEMPOTENCY_KEY_REUSED",
"message": "The idempotency key was already used with a different request."
}
}
keyのscopeは少なくとも
authenticated subject + org_id + endpoint + Idempotency-Key
とします。
Idempotency-Key がない場合は、必須headerのvalidation failureとして暫定的に 422 とします。
7. 正常JSON例
作成
http
POST /expenses
→ 201 Created
JSON
{
"id": "exp_123",
"amount": 12800,
"category": "travel",
"memo": "東京出張",
"status": "draft",
"created_at": "2026-09-16T06:30:00+09:00",
"owner_name": "山田 太郎"
}
一覧
http
GET /expenses?status=submitted&month=2026-09
→ 200 OK
JSON
{
"items": [
{
"id": "exp_456",
"amount": 3500,
"category": "meal",
"memo": "取引先との昼食",
"status": "submitted",
"created_at": "2026-09-12T14:20:00+09:00",
"owner_name": "山田 太郎"
}
]
}
申請
http
POST /expenses/exp_123/submit
→ 200 OK
JSON
{
"id": "exp_123",
"amount": 12800,
"category": "travel",
"memo": "東京出張",
"status": "submitted",
"created_at": "2026-09-16T06:30:00+09:00",
"owner_name": "山田 太郎"
}
削除
http
DELETE /expenses/exp_123
→ 204 No Content
204ではbodyを返しません。
8. 失敗JSON例
未認証
http
401 Unauthorized
WWW-Authenticate: Bearer
JSON
{
"error": {
"code": "UNAUTHORIZED",
"message": "Authentication is required."
}
}
JWTの失効、署名不正、issuer/audience不一致などについて、外部responseで詳細原因を知らせる必要はありません。
Validation
http
422 Unprocessable Content
JSON
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed."
}
}
たとえば、
JSON
{
"amount": 0,
"category": "hotel",
"memo": "..."
}
は422です。
FastAPI標準のvalidation error bodyはこの既存契約と違うため、実装時にはexception handlerが必要になります。
他人のID / 存在しないID
両方とも同じです。
http
404 Not Found
JSON
{
"error": {
"code": "EXPENSE_NOT_FOUND",
"message": "Expense not found."
}
}
Role不足
resource自体を見る資格がある場合に限り403です。
http
403 Forbidden
JSON
{
"error": {
"code": "FORBIDDEN",
"message": "You are not allowed to perform this action."
}
}
状態競合
http
409 Conflict
JSON
{
"error": {
"code": "INVALID_STATE_TRANSITION",
"message": "The expense state does not allow this operation."
}
}
9. Error matrix
条件 HTTP error.code 備考
Authorizationなし 401 UNAUTHORIZED Bearer要求
JWT不正/期限切れ 401 UNAUTHORIZED 詳細をbodyに出さない
amount範囲外 422 VALIDATION_ERROR
category不正 422 VALIDATION_ERROR
memo > 200 422 VALIDATION_ERROR
receipt_url がHTTP 422 VALIDATION_ERROR HTTPSのみ
status query不正 422 VALIDATION_ERROR
month形式不正 422 VALIDATION_ERROR
createでIdempotency-Key欠落 422 VALIDATION_ERROR 暫定契約
Idempotency-Key再利用+異なるpayload 409 IDEMPOTENCY_KEY_REUSED
IDが存在しない 404 EXPENSE_NOT_FOUND
memberが他人のIDを指定 404 EXPENSE_NOT_FOUND 存在を秘匿
adminが別orgのIDを指定 404 EXPENSE_NOT_FOUND 存在を秘匿
同一org adminが他人の申請をGET 200 — 明示的に許可
同一org adminが他人の申請をPATCH 403 FORBIDDEN 見る権限はあるが編集権限なし
memberが自分の申請をapprove 403 FORBIDDEN role不足
ownerがsubmittedをPATCH 409 INVALID_STATE_TRANSITION roleではなく状態
ownerがapprovedをDELETE 409 INVALID_STATE_TRANSITION
adminがdraftをapprove 409 INVALID_STATE_TRANSITION submittedのみ
adminがapprovedを再approve 409 INVALID_STATE_TRANSITION
重要なのは、
「存在しない」
「存在するが、その利用者から見えない」
を同じ404にすることです。
10. 認証・所有権・validationテスト
Test 条件 Expected
AUTH-01 Tokenなしで GET /expenses 401 UNAUTHORIZED
AUTH-02 期限切れJWT 401 UNAUTHORIZED
AUTH-03 不正署名JWT 401 UNAUTHORIZED
AUTH-04 JWTのorg_idを書換えたtoken 401
OWNER-01 member A が自分のexpenseをGET 200
OWNER-02 member A がmember Bの有効なexpense_idをGET 404
OWNER-03 member A がmember Bのexpense_idをPATCH 404
OWNER-04 member A がmember Bのexpense_idをDELETE 404
OWNER-05 存在しないexpense_idをmember AがGET 404、OWNER-02と同一body
ORG-01 admin A が同一orgのmember BのexpenseをGET 200
ORG-02 admin A が別orgの実在expense_idをGET 404
ORG-03 同一org adminが他人のexpenseをPATCH 403
ROLE-01 memberが自分のsubmitted expenseをapprove 403
ROLE-02 memberが自分のsubmitted expenseをreturn 403
STATE-01 draftをownerがsubmit 200、submitted
STATE-02 returnedをownerが再submit 200、submitted
STATE-03 submittedをownerがPATCH 409
STATE-04 approvedをownerがPATCH 409
STATE-05 approvedをownerがDELETE 409
STATE-06 draftをadminがapprove 409
STATE-07 submittedをadminがapprove 200、approved
STATE-08 submittedをadminがreturn 200、returned
VAL-01 amount=0 422
VAL-02 amount=1 success
VAL-03 amount=1,000,000 success
VAL-04 amount=1,000,001 422
VAL-05 amount=true 422
VAL-06 category=hotel 422
VAL-07 memo="" success
VAL-08 memo 200文字 success
VAL-09 memo 201文字 422
VAL-10 http://... receipt_url 422
VAL-11 https://... receipt_url success
VAL-12 month=2026-9 422
VAL-13 month=2026-09 success
IDEM-01 同じkey・同じcreateを2回 同じid、1件だけ作成
IDEM-02 同じkey・payload変更 409
IDEM-03 別ownerが同じkey 別scopeとして扱う
特に OWNER-02〜04 と ORG-02 は、今回の秘匿要件に対する必須受入テストです。
11. エラー判定の優先順位
競合しやすいケースについて、契約上は次の優先順位が安全です。
1. Authentication
2. Requestの構文・schema validation
3. resource visibility
4. action authorization
5. state transition
6. business operation
したがって、有効なrequestについては、
他人のID → 404
自分のIDだがrole不足 → 403
自分のIDでroleも正しいが状態違反 → 409
となります。
ただしFastAPIの自動Pydantic validationをそのまま使うと、body不正の場合はobject lookupより先に422になることがあります。
これは例えば、
他人のexpense_id + amount=0
でも422になる可能性があるということです。
この挙動自体から「そのIDが存在するか」は判別できないため、今回の秘匿条件とは矛盾しません。存在する他人IDと存在しないIDに対し、同じ入力なら同じ結果になることをテストすべきです。
12. HTTP status / body整合性チェック
今回の契約では矛盾はありません。
HTTP 意味 Body
200 GET/更新/状態遷移成功 success JSON
201 新規expense作成成功 success JSON
204 DELETE成功 bodyなし
401 認証失敗 error JSON
403 objectは見えるが操作権限なし error JSON
404 nonexistent または invisible object 同一error JSON
409 resource状態/idempotency競合 error JSON
422 request schema不正 error JSON
特に避けるべきなのは 204 + JSON body、200 + {"error":...}、validationでFastAPI標準形式と既存API形式が混在する状態です。
13. 未確定事項
実装前に決める必要があるのは次の点です。
admin自身の申請をadmin自身が承認できるか。 現契約だけなら可能ですが、経費承認では自己承認禁止にするケースがあります。禁止なら admin + owner の approve/return を403にします。
adminはmember能力も持つか。 上表では「adminも自分のexpenseを作成・編集・申請できる」と仮定しています。adminが純粋な承認者なら変更が必要です。
month が何の月か。 現在resourceに expense_date がないため、候補は created_at の月です。ただし「9月に使った8月分経費」を扱うなら業務要件として不十分なので、expense_date が必要です。
month判定のtimezone。 社内業務が日本限定なら Asia/Tokyo 固定が候補ですが、契約として明記が必要です。
submittedの削除/取り下げ。 今回は不可としました。必要なら withdraw の状態遷移を追加する方が、DELETEで申請取消を表現するより明確です。
receipt_url: null をPATCHで送った場合。 「削除する」のか「不正」とするのかを決める必要があります。省略は「変更なし」で問題ありません。
Idempotency-Key保持期間。 24時間などのTTLを決める必要があります。通信再送防止だけなら24時間程度が候補ですが、業務側の再送期間から決めるべきです。
expense_id形式。 UUID、ULID、独自opaque IDのどれか。外部利用者にDB連番を直接公開しない形が適しています。
一覧のpagination。 件数上限、cursor方式、total を返すかが未定です。adminが全組織申請を閲覧するため、実装前に上限を決めた方が安全です。
この契約なら、次にFastAPI実装へ進む際は Pydantic schema → JWT dependency → visible-object lookup → authorization → state transition service → custom exception handler → idempotency storage の境界をかなり明確に分けられます。
この出力になっていれば成功
- 各endpointにauth/owner・正常/主要失敗がある
- statusとbody整合
- 秘密非露出
- 他人IDテスト
使うときの注意
実行前にGit差分またはバックアップを確認し、テスト環境で検証してください。
権限/所有関係や既存error契約が未定なら保留して質問。403/404等を独断で固定しない。
参考にした一次情報: Python Docs / Stack Overflow