エラーカタログ & 構造化ログ Error Catalog & Structured Logging
Parky BFF(Cloudflare Workers)が返すすべての HTTP エラーコードと、
観測性のために吐く 1 行 JSON ログの仕様をまとめます。
実体は api/src/lib/errors.ts(ERROR_CATALOG /
ApiError / ショートハンド) と api/src/lib/logger.ts、
正規化と response 整形は api/src/middleware/error-handler.ts。
The single source of truth for every HTTP error code returned by the Parky BFF
(Cloudflare Workers) and for the one-line JSON log format. Code lives in
api/src/lib/errors.ts (ERROR_CATALOG / ApiError /
shortcuts), api/src/lib/logger.ts, and the normalization + response
shaping happens in api/src/middleware/error-handler.ts.
api/src/lib/errors.ts の
ERROR_CATALOG にだけ追加する。このドキュメントは catalog に揃える。
When adding a new code, touch only ERROR_CATALOG in
api/src/lib/errors.ts. This document tracks the catalog.
1. 応答フォーマット1. Response format
すべてのエラー応答は統一形式。ステータスコードは code から決まる(ルート側で status を指定する必要はない)。
Every error response uses the same shape. Status is derived from code — route handlers never pass status explicitly.
// 実装: middleware/error-handler.ts (B-01 Stripe-tier envelope)
{
"error": {
"code": "not_found", // ErrorCode(下表参照)
"message": "parking_lot not found", // 人間可読メッセージ
"message_key": "errors.not_found", // i18n key(lib/i18n-keys.ts → ApiError.messageKey で override 可)
"request_id": "018f3b7a-4b5c-4c4b-...", // x-request-id ヘッダと一致
"param": null, // ZodError なら issues[0].path[0]、それ以外 null
"doc_url": null // ERROR_CATALOG[code].doc_url(一部 code のみ)
}
}
message_key / param / doc_url は B-01 (2026-04-28) で追加。
旧クライアントとの互換のため optional + nullable で常時付与。doc_url 形式は
https://docs.parky.co.jp/errors/<code>。
message_key / param / doc_url were added in B-01
(2026-04-28). They are always emitted (optional + nullable) for backward compatibility.
doc_url follows https://docs.parky.co.jp/errors/<code>.
1.1 throw → response の流れ1.1 throw → response flow
flowchart LR R[route handler
throw badRequest / ApiError / HTTPException] --> EH[middleware/
error-handler.ts] EH --> N[normalizeToApiError] N -->|HTTPException| F[fromHTTPException] N -->|Error その他| INT[ApiError 'internal_error' 500] F --> AE[ApiError] N -->|ApiError| AE AE --> RP[buildApiErrorResponse
code / status / message_key / param / doc_url / request_id] AE -->|status >= 500| SEN[Sentry capture] AE -->|prod| RED[redactErrorForProduction
stack/cause/detail を落とす] RED --> LOG[logger warn 4xx / error 5xx] RP --> CLIENT[Client]
クライアントは code で機械的に分岐し、message は UI に表示しない(i18n 対応のため、
ユーザー向け文言はクライアント側で code に対応するローカライズ文字列を用意する方針)。
request_id はサーバーログと突合するキー。
Clients branch on code programmatically and do not render message as-is
(for i18n, map each code to a localized string on the client side). request_id
correlates with server logs.
2. エラーコード一覧(ERROR_CATALOG)2. Error codes (ERROR_CATALOG)
2-1. 汎用 HTTP コード2-1. Generic HTTP codes
code |
status | 用途 | Purpose | 代表的な投げ場所 | Typical call site |
|---|---|---|---|---|---|
bad_request | 400 | 入力バリデーション / 業務エラーの総合枠 | Input validation and miscellaneous client errors | throw badRequest("...") |
throw badRequest("...") |
unauthorized | 401 | Bearer トークン欠如 / 検証失敗 | Missing or invalid bearer token | middleware/auth.ts |
middleware/auth.ts |
forbidden | 403 | 認証済みだが権限不足(非 admin / 停止中 admin 等) | Authenticated but lacking permission (non-admin, suspended admin, etc.) | requireAdmin, RBAC |
requireAdmin, RBAC |
not_found | 404 | リソース非存在 / Postgrest PGRST116 |
Missing resource / Postgrest PGRST116 |
throw notFound("...") |
throw notFound("...") |
conflict | 409 | 一意制約違反(Postgres 23505) |
Unique-constraint violation (Postgres 23505) |
translatePgError |
translatePgError |
unprocessable_entity | 422 | Zod バリデーション失敗(OpenAPIHono defaultHook) |
Zod validation failure (OpenAPIHono defaultHook) |
src/index.ts の defaultHook |
defaultHook in src/index.ts |
rate_limited | 429 | Cloudflare Rate Limiter / ユーザー単位抑制 | Cloudflare Rate Limiter on /v1/search/ai etc. |
RATE_LIMIT_USER binding |
RATE_LIMIT_USER binding |
internal_error | 500 | 想定外の例外 / PostgresError で特定 code 無し | Unexpected exception / generic PostgresError | error-handler のフォールバック | error-handler fallback |
bad_gateway | 502 | 上流 API 失敗(LLM / Workers AI / FCM 等) | Upstream API failure (LLM, Workers AI, FCM, etc.) | throw badGateway("...") |
throw badGateway("...") |
service_unavailable | 503 | binding 未設定 / 一時的不稼動 | Binding not configured / temporary outage | throw serviceUnavailable("...") |
throw serviceUnavailable("...") |
method_not_allowed | 405 | OpenAPI method 違反 / CORS preflight 後の許可外 method | OpenAPI method violation / disallowed method after CORS preflight | throw methodNotAllowed("...") |
throw methodNotAllowed("...") |
timeout | 504 | 外部 API / DB の上流タイムアウト(retryable: true) |
Upstream timeout from external API / DB (retryable: true) |
new ApiError("timeout", "...") |
new ApiError("timeout", "...") |
各 entry は retryable / clientAction(reauthenticate /
retry_after / upgrade_plan / contact_support) / doc_url
を任意で持つ(M-24 拡張、E-4 doc URL)。SDK はこれを参照して自動再送 / 再認証 / 課金導線を選択する。
Each entry can carry optional retryable / clientAction
(reauthenticate / retry_after / upgrade_plan /
contact_support) / doc_url (M-24 + E-4). SDKs use these to drive
auto-retry, re-auth, or upgrade flows.
2-2. 業務固有コード2-2. Business-specific codes
汎用コードに寄せきれない「UI がコードで分岐したい業務エラー」はカタログに個別追加する。 命名は英語小文字スネークケース、status は最も近い HTTP 区分に合わせる。
Business errors that clients must branch on by code (beyond the generic codes) are added individually. Names are lowercase snake_case and status follows the nearest HTTP category.
code |
status | 意味 | Meaning | 発生箇所 / PG ERRCODE | Origin / PG ERRCODE | クライアントの期待動作 | Expected client behavior |
|---|---|---|---|---|---|---|---|
iap_not_configured | 503 | IAP 用の Apple / Google シークレットが BFF に未投入 | IAP secrets (Apple / Google) not yet provisioned on the BFF | lib/iap/index.ts |
lib/iap/index.ts |
「現在課金処理を受け付けていません」相当のガイダンスを表示 | Show a "billing temporarily unavailable" message |
unknown_product | 422 | クライアントから来た productId が subscription_plans に無い |
productId from client has no matching row in subscription_plans |
lib/iap/index.ts |
lib/iap/index.ts |
アプリのプラン表を再取得(マスター不整合) | Refresh the plan master on the client (config drift) |
app_user_not_found | 404 | JWT の auth_user_id に対応する app_users 行なし |
No app_users row matches the authenticated auth_user_id |
P0001 |
P0001 |
onboarding 画面へ。プロフィール自動生成フローを再実行 | Route to onboarding; re-run the profile bootstrap flow |
parking_lot_not_found | 404 | 指定された parking_lot_id が存在しない / 削除済み |
parking_lot_id does not exist or was soft-deleted |
P0002 |
P0002 |
キャッシュされた id を破棄して再検索 | Drop the cached id and retry search |
parking_session_not_found | 404 | セッション id 未存在 / 他人のセッション | Session id does not exist or belongs to another user | P0004 |
P0004 |
履歴画面に戻る | Navigate back to history |
concurrent_session_in_progress | 409 | 既に status='parking' のセッションが存在する |
An active status='parking' session already exists |
P0003 |
P0003 |
進行中セッションを表示し、finalize / cancel を促す | Surface the in-progress session and prompt to finalize / cancel |
invalid_session_state | 409 | 状態遷移が不正(例: completed を finalize しようとした) |
Illegal state transition (e.g. finalize on completed) |
P0006 |
P0006 |
セッション詳細を再取得して UI を同期 | Refetch session detail to reconcile UI |
cancel_window_expired | 410 | 開始から 5 分を超過し、cancel 不可 | More than 5 min since start; cancel no longer allowed | P0007 |
P0007 |
「5 分を過ぎたためキャンセルできません」→ finalize を案内 | "Cancel window expired" — direct user to finalize instead |
idempotency_conflict | 409 | 同じ client_request_id で異なるペイロードを送った等、冪等性違反 |
Same client_request_id reused with a different payload |
ルート側で検出 | Detected at route layer | クライアントのキー生成ロジックを要修正 | Fix the client's key-generation logic |
subscription_required | 402 | 有料機能へのアクセスを無料プランで試みた | Free-plan user tried to access a paid feature | ルート側で plan チェック(今後の拡張点) | Plan check in route handlers (future hook) | 課金モーダル / プラン比較画面へ誘導 | Route to the pricing / upgrade screen |
plan_limit_exceeded | 409 | プランの上限(車両登録数 / 保存駐車場数 等)に到達 | Hit a plan-specific cap (vehicles, saved lots, etc.) | ルート側で count & check | Count + check in route handlers | 「上限に達しました」メッセージ + アップグレード誘導 | Show "limit reached" UI with an upgrade CTA |
owner_not_found | 403 | JWT の auth user が owners テーブルに紐付いていない |
The authenticated user has no matching row in owners |
middleware requireOwner |
middleware requireOwner |
オーナー申請画面へ誘導 | Route to the owner-application flow |
owner_status_inactive | 403 | owners.status が active ではない(reviewing / suspended) |
owners.status is not active (reviewing / suspended) |
middleware requireOwner |
middleware requireOwner |
審査中 / 停止のステータス画面を表示 | Show the "under review" / "suspended" state screen |
owner_not_lot_owner | 403 | 自分が所有していない駐車場を触ろうとした | Tried to act on a parking lot the current owner does not own | /v1/owner/* のルート側チェック |
Route-level check in /v1/owner/* |
権限エラーとしてメッセージを表示 | Display as a permission error |
owner_application_duplicate | 400 | 同じ駐車場に「審査中」の申請が既にある | An in-review application already exists for this parking lot | /v1/owner/authority-requests |
/v1/owner/authority-requests |
既存申請の詳細画面へ誘導 | Surface the existing application's detail |
owner_review_not_for_your_lot | 403 | 自分の駐車場以外のレビューに返信しようとした | Tried to reply to a review on a parking lot you do not own | /v1/owner/reviews/{reviewId}/reply |
/v1/owner/reviews/{reviewId}/reply |
権限エラーとしてメッセージ | Display as a permission error |
owner_boost_not_found | 404 | ブーストが存在しない or 自分のブーストではない | Boost does not exist or is not yours | /v1/owner/boosts/* |
/v1/owner/boosts/* |
一覧画面に戻す | Return to the boost list |
auth_gate_required | 401 | Anonymous で Free 必須機能を叩いた | Anonymous user hit a Free-only feature | lib/auth-gate.ts |
lib/auth-gate.ts |
AuthGateModal 表示 → 登録後に元操作 retry | Show AuthGateModal, retry the original action after sign-up |
premium_required | 402 | Free 会員が Premium 必須機能を叩いた | Free-tier user hit a Premium-only feature | route plan check | Route plan check | Premium 訴求モーダル / 課金導線 | Show Premium upsell modal / pricing CTA |
trial_already_used / variant_not_assigned | 409 | トライアル既使用 / プラン variant 未割当 | Trial already consumed / pricing variant not assigned | core/membership | core/membership | プラン画面でステータスを表示 | Surface the status on the pricing screen |
owner_password_token_invalid /
token_expired / already_used / invalidated
| 410 | パスワード設定リンクが無効 / 期限切れ / 使用済 / 無効化 | Password setup link is invalid / expired / already used / invalidated | /v1/owner-public/password-setup |
/v1/owner-public/password-setup |
「リンク再発行を依頼」を案内 | Prompt to request a fresh link |
token_already_consumed | 410 | single-use 消費済トークンの再投入(漏洩検知) | Single-use token replayed (leak detection) | DB レイヤで弾く | Blocked at DB layer | サポート連絡を案内(contact_support) |
Direct user to support (contact_support) |
owner_password_update_failed | 500 | /complete 時の Supabase Auth password 更新失敗(token は未消費のまま) | Supabase Auth password update failed at /complete (token left unconsumed) | /v1/owner-public/password-setup/complete |
/v1/owner-public/password-setup/complete |
リトライを促す | Prompt the user to retry |
otp_expired / otp_invalid | 422 | OTP の期限切れ / 値不一致 | OTP expired / value mismatch | auth route | Auth route | 再送 OTP を促す | Prompt to resend OTP |
email_already_exists / email_registered_via_oauth | 409 | 同 email 重複 / 別 OAuth プロバイダで登録済 | Duplicate email / already registered via different OAuth provider | auth signUp | Auth signUp | 該当プロバイダでのログイン誘導 | Route to the correct provider's sign-in flow |
account_blocked | 403 | 運営側ブロック email/account の再ログイン拒否 | Operator-blocked email/account refused at sign-in | auth route | Auth route | サポート連絡を案内 | Direct user to support |
review_already_exists / prohibited_words_detected |
409 / 422 | 同 lot に既存レビュー / 禁止ワード検出 | Existing review on the same lot / prohibited word detected | /v1/me/reviews |
/v1/me/reviews |
既存レビュー編集 or 文言修正を促す | Prompt to edit existing review or revise wording |
iap_receipt_invalid | 422 | IAP レシートが無効 / 改ざん / 期限切れ | IAP receipt is invalid / tampered / expired | /v1/webhooks/apple-iap / google-play |
/v1/webhooks/apple-iap / google-play |
サポート連絡 + 復旧導線 | Direct to support + recovery flow |
coupon_not_found / coupon_expired / coupon_usage_limit_exceeded |
404 / 422 / 409 | クーポンの未存在 / 期限切れ / 上限到達 | Coupon missing / expired / over usage cap | premium actions | Premium actions | 該当エラーを UI に表示 | Surface as a coupon error |
preset_limit_exceeded / vehicle_limit_exceeded | 409 | プリセット 10 件 / 車両 10 台の上限到達 | Hit the 10-preset / 10-vehicle plan cap | core/saved / core/vehicles | core/saved / core/vehicles | アップグレード CTA を表示 | Show upgrade CTA |
lot_registration_duplicate / subscription_not_found |
409 / 404 | 同 user + 駐車場名 + 住所の重複申請 / アクティブ subscription 不在 | Duplicate lot registration request / no active subscription | premium actions | Premium actions | 既存申請 / プラン状態を表示 | Surface the existing request / plan state |
invalid_delete_confirmation / account_already_deleted |
422 / 409 | delete-account の confirmation 不一致 / 二重削除 | delete-account confirmation mismatch / duplicate delete | /v1/mobile/actions/profile/delete-account |
/v1/mobile/actions/profile/delete-account |
確認文言入力をやり直し | Re-prompt for the confirmation phrase |
invalid_planned_end_at / invalid_started_at /
invalid_user_entered_fee / invalid_memo
|
422 | セッション開始/finalize/メモ更新の値域違反 | Range / format violations on session create / finalize / memo update | core/parking-sessions | core/parking-sessions | 入力フォームに inline error 表示 | Surface as inline form errors |
lot_not_navigable | 409 | 座標を持たない駐車場のナビ起動要求 | Navigation requested on a lot without coordinates | /v1/mobile/actions/lots/{id}/navigate |
/v1/mobile/actions/lots/{id}/navigate |
「住所のみのため起動不可」を表示 | Show "address-only, navigation unavailable" |
session_notification_not_found / notification_already_fired /
notification_cancelled / notification_update_failed /
planned_end_at_required
|
404 / 409 | セッション通知の状態競合 / 前提欠落 | Session-notification state conflicts / missing prerequisites | core/session-notifications | core/session-notifications | 通知一覧を再取得して UI を同期 | Refetch notifications to reconcile UI |
invalid_threshold_amount / invalid_threshold_at /
invalid_offset_minutes
|
422 | price_reached / time_reached / planned_end_minus の値域違反 | Value-range violations on price_reached / time_reached / planned_end_minus | core/session-notifications | core/session-notifications | 入力フォームに inline error 表示 | Surface as inline form errors |
fcm_not_configured / queue_not_configured | 503 | FCM / Queue binding 未投入(環境構築不全) | FCM / Queue binding not provisioned (env misconfig) | 各種 send / dispatch 経路 | Send / dispatch paths | サポート連絡を案内 | Direct user to support |
asset_not_found / vehicle_not_found | 404 | avatar asset / vehicle が未存在 or 自分のものではない | Avatar asset / vehicle missing or not owned by caller | profile actions | Profile actions | 一覧を再取得して破棄 | Refetch the list and drop the stale id |
owner_required | 401 | withOwnerContext() 未通過 handler で c.var.ownerId 未解決 |
c.var.ownerId unresolved when withOwnerContext() wasn't applied |
middleware/owner-context.ts |
middleware/owner-context.ts |
再認証フローへ | Route to re-authentication |
owner_boost_create_failed | 500 | ブースト作成 SQL の失敗(DB 例外を 500 で転嫁) | Boost-create SQL failure (DB exception surfaced as 500) | /v1/owner/boosts |
/v1/owner/boosts |
retryable=true なので自動再送可 | retryable=true so SDK can auto-retry |
RAISE EXCEPTION '...' USING ERRCODE = 'P00XX' で投げるシグナルは、
lib/db.ts の translatePgError がこの表の P00XX 欄に従って
自動的に ApiError へ写像する。新しい P00XX を定義したら必ずこの表と translatePgError を
両方更新すること。
Signals raised from PL/pgSQL via RAISE EXCEPTION '...' USING ERRCODE = 'P00XX'
are mapped to ApiError automatically by translatePgError in
lib/db.ts. When adding a new P00XX, update both this table and
translatePgError.
3. コード投げ方(コード側の書き方)3. How to throw from code
ルート / ミドルウェア / ライブラリから投げる時は、ショートハンド関数か
ApiError 直接インスタンス化の二通り。生の HTTPException は使わない。
Throw either via a shortcut function or by constructing ApiError directly.
Never use raw HTTPException.
import {
badRequest, unauthorized, forbidden,
notFound, conflict, unprocessable,
rateLimited, badGateway, serviceUnavailable,
ApiError,
} from "../lib/errors";
// 1) ショートハンド(推奨)
if (!body.name) throw badRequest("name is required");
if (!token) throw unauthorized("Missing bearer token");
if (row == null) throw notFound();
// 2) 業務コードは ApiError を直接
if (!creds) throw new ApiError("iap_not_configured");
if (!plan) throw new ApiError("unknown_product", `productId=${id}`);
// 3) 原因例外は第 2/3 引数の cause に積む(ログの err.cause に出る)
try {
await upstream();
} catch (err) {
throw badGateway("LLM call failed", err);
}
throw new HTTPException(...)を直接書かない。全てショートハンド/ApiError 経由で統一する- Don't write
throw new HTTPException(...)directly — always go through a shortcut orApiError. - ルートハンドラ内で
c.json({ error: {...} }, 4xx)を手組みしない。throw に統一 - Don't hand-assemble
c.json({ error: {...} }, 4xx)in routes — always throw instead. - エラーメッセージに UUID / メールアドレス / トークン等の個人情報を入れない
- Don't put UUIDs / emails / tokens in error messages.
4. 構造化ログ(lib/logger.ts)4. Structured logging (lib/logger.ts)
Workers の stdout は Cloudflare Logs / Tail / Logpush にそのまま流れるので、
すべてのログは 1 行 JSONで吐く。console.* の直接呼出しは禁止
(lib/logger.ts 内部のみ許可)。
Workers stdout flows to Cloudflare Logs / Tail / Logpush verbatim, so every log line is
one-line JSON. Direct console.* calls are disallowed (allowed only inside lib/logger.ts).
4-1. 出力スキーマ4-1. Output schema
| フィールド | Field | 型 / 例 | Type / example | 備考 | Notes |
|---|---|---|---|---|---|
ts | ts |
ISO8601 / 2026-04-19T12:34:56.789Z |
ISO8601 / 2026-04-19T12:34:56.789Z |
UTC。常に含まれる | UTC; always present |
level | level |
"debug" | "info" | "warn" | "error" |
"debug" | "info" | "warn" | "error" |
LOG_LEVEL でしきい値フィルタ(dev=debug / prod=info) |
Gated by LOG_LEVEL (dev=debug / prod=info) |
msg | msg |
string / "request completed" |
string / "request completed" |
人間可読。英語推奨(日本語も可) | Human-readable; English preferred |
scope | scope |
"http" | "cron.warmer" | "queue.store-sync" | "lib.llm" | ... |
"http" | "cron.warmer" | "queue.store-sync" | "lib.llm" | ... |
発火元を識別。{category}.{name} 形式で統一 |
Origin identifier. Use {category}.{name} consistently |
request_id | request_id |
UUID | UUID | HTTP 経由は必ず付与。x-request-id と一致 |
Always set for HTTP; matches x-request-id |
err | err |
{ name, message, code?, status?, stack?, cause? } |
{ name, message, code?, status?, stack?, cause? } |
warn/error 時のみ。ApiError は code/status 含む |
Emitted on warn/error; ApiError contributes code/status |
| 任意のフィールド | Arbitrary fields | status, latency_ms, sync_run_id, user_id, provider, ... |
status, latency_ms, sync_run_id, user_id, provider, ... |
検索しやすいように snake_case 推奨。自由記述 | snake_case recommended for filtering; free-form |
4-2. 出力例4-2. Sample lines
// HTTP アクセスログ(毎リクエスト末尾に 1 行)
{"ts":"2026-04-19T12:34:56.789Z","level":"info","msg":"request completed",
"scope":"http","request_id":"018f...","method":"GET","path":"/v1/parking-lots",
"status":200,"latency_ms":42}
// 4xx はアクセスログ前に warn が 1 行
{"ts":"...","level":"warn","msg":"request failed","scope":"http",
"request_id":"018f...","method":"POST","path":"/v1/me/vehicles",
"status":422,"error_code":"unprocessable_entity",
"err":{"name":"ApiError","message":"Validation failed","code":"unprocessable_entity","status":422}}
// Cron
{"ts":"...","level":"info","msg":"done","scope":"cron.sponsor-proximity",
"sponsors_evaluated":12,"users_matched":48,"notifications_sent":45,"failed":3,
"elapsed_ms":1523}
// Queue consumer
{"ts":"...","level":"info","msg":"run finished","scope":"queue.store-sync",
"sync_run_id":"018f...","store":"app_store","task":"reviews",
"status":"success","rows_upserted":37}
4-3. ログの書き方4-3. How to log
// 1) HTTP ルート(c.var.log が request_id 付き logger)
app.get("/v1/foo", (c) => {
c.var.log.info("foo fetched", { foo_id });
c.var.log.warn("fallback triggered", { reason });
c.var.log.error("db query failed", err, { query_id });
});
// 2) cron / queue / library
import { createLogger } from "../lib/logger";
const log = createLogger(env, { scope: "cron.my-job", run_id });
log.info("started");
log.error("task failed", err);
// 3) child logger で context を追加
const childLog = log.child({ sync_run_id });
childLog.info("sub-task done");
5. 分析クエリ例5. Analysis queries
Cloudflare Tail / Logs Engine / Logpush 先(S3/Loki 等)で jq を叩く想定。
Assuming jq against Cloudflare Tail / Logs Engine / Logpush sinks (S3, Loki, etc).
# 5xx のみ抽出
jq 'select(.level=="error")'
# 特定 request_id でトレース(HTTP → LLM → DB)
jq 'select(.request_id=="018f3b7a-...")'
# Queue 系だけを時系列で
jq 'select(.scope | startswith("queue."))'
# 特定の業務コードを集計
jq 'select(.err.code=="iap_not_configured")' | jq -s 'length'
# p95 レイテンシ(アクセスログ)
jq -r 'select(.msg=="request completed") | .latency_ms' \
| sort -n | awk 'NR==int(NR*0.95){print}'
# scope 別のエラー件数
jq -r 'select(.level=="error") | .scope' | sort | uniq -c | sort -rn
6. 新しいコードを追加する手順6. Adding a new code
-
api/src/lib/errors.tsのERROR_CATALOGに{ status, message }を追加する。命名は英語小文字スネークケース。 -
Add
{ status, message }toERROR_CATALOGinapi/src/lib/errors.ts. Name in lowercase snake_case. -
頻繁に使うならショートハンド関数(
badRequest等と同じパターン)を追加する。 -
Add a shortcut function if it'll be used frequently (follow the
badRequestpattern). -
呼出側で
throw new ApiError("new_code", "...")or ショートハンドを使う。 -
Throw via
throw new ApiError("new_code", "...")or the shortcut. -
このドキュメント(
docs/api-errors.html)の該当テーブルに行を追加する。 -
Append a row to the table in this document (
docs/api-errors.html). - クライアント側(モバイル / admin / public web)でハンドリング方針を決めて実装する。
- Decide and implement the client-side handling (mobile / admin / public web).