This skill should be used when the user is designing API error responses, choosing HTTP status codes, implementing error envelopes, handling validation errors, creating per-field error messages, or following RFC 9457 Problem Details. Covers status code selection (2xx-5xx), consistent error formats, Stripe/GitHub/Twilio error patterns, and request/trace ID correlation.
npx claudepluginhub oborchers/fractional-cto --plugin api-design-principlesThis skill uses the workspace's default tool permissions.
A well-designed error response teaches the developer what went wrong, points them to the fix, and gives their code enough information to handle it automatically. Bad errors generate support tickets. Good errors eliminate them.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
Retrieves current documentation, API references, and code examples for libraries, frameworks, SDKs, CLIs, and services via Context7 CLI. Ideal for API syntax, configs, migrations, and setup queries.
Uses ctx7 CLI to fetch current library docs, manage AI coding skills (install/search/generate), and configure Context7 MCP for AI editors.
A well-designed error response teaches the developer what went wrong, points them to the fix, and gives their code enough information to handle it automatically. Bad errors generate support tickets. Good errors eliminate them.
The HTTP status code is the first thing a client reads. It must be accurate, specific, and machine-actionable. The status code conveys the category of outcome; the response body conveys the detail. Never return 200 with { "success": false } -- it breaks HTTP semantics, defeats monitoring, and confuses every layer of infrastructure between your server and the client.
Pick the most specific code that applies. When in doubt, move down the table.
| Code | When to Use |
|---|---|
| 200 OK | GET, PUT, PATCH, and POST actions that are not resource creation. Return the resource. |
| 201 Created | A new resource was created. Return the resource and a Location header with its URI. |
| 202 Accepted | Async work was queued but not yet complete. Return a status object with a job URL. |
| 204 No Content | Success with intentionally no body. Use for DELETE. Also valid for PUT/PATCH when you omit the updated resource. |
| Code | When to Use |
|---|---|
| 301 Moved Permanently | Resource has a new canonical URI. Clients should update bookmarks. |
| 304 Not Modified | Resource unchanged since If-None-Match or If-Modified-Since. Critical for caching. |
| 307 Temporary Redirect | Temporary reroute, method and body preserved. |
| Code | When to Use |
|---|---|
| 400 Bad Request | Malformed request. Unparseable JSON, missing required top-level fields, wrong content type. The problem is syntactic. |
| 401 Unauthorized | Authentication failed or missing. "Who are you?" |
| 403 Forbidden | Authenticated but not authorized. "I know who you are, and you cannot do this." |
| 404 Not Found | Resource does not exist. Also use 404 instead of 403 to hide resource existence from unauthorized callers. |
| 405 Method Not Allowed | HTTP method not supported. Include an Allow header listing valid methods. |
| 409 Conflict | State conflict -- duplicate resource, concurrent modification, invalid state transition. |
| 422 Unprocessable Content | Syntactically valid but semantically invalid. Business rule violations, field-level validation failures. |
| 429 Too Many Requests | Rate limit exceeded. Must include Retry-After header. |
| Code | When to Use |
|---|---|
| 500 Internal Server Error | Unhandled exception, bug in server code. Never expose stack traces. |
| 502 Bad Gateway | Upstream service returned an invalid response. |
| 503 Service Unavailable | Temporarily unable to handle requests. Include Retry-After. |
| 504 Gateway Timeout | Upstream service timed out. |
Retry guidance: Clients should retry on 429, 503, and 504 with exponential backoff. They should not blindly retry on 500 (may not be idempotent-safe).
Use 400 when the request is structurally broken -- unparseable JSON, missing content type, absent required fields. Use 422 when the JSON is well-formed and all required fields are present, but the data itself is invalid -- email is not a valid email, start_date is after end_date, quantity is negative.
This distinction matters because it tells the client whether the problem is a bug in their request construction (400) or invalid user input that should be surfaced in a form (422).
Use 401 when the client has not proven its identity -- missing token, expired token, invalid API key. Re-authenticating can fix it. Use 403 when the client is authenticated but lacks the required permissions. Re-authenticating will not help; they need different access rights.
Security exception: When a client requests a resource they are not even authorized to know about, return 404 instead of 403 to prevent enumeration attacks. GitHub does this -- accessing a private repository you are not a member of returns 404.
Use one consistent structure for every error response across every endpoint. Clients should never need to handle multiple error formats.
{
"type": "https://api.example.com/errors/validation-error",
"title": "Validation Error",
"status": 422,
"detail": "2 fields failed validation.",
"code": "validation_error",
"instance": "urn:request:a1b2c3d4-e5f6-7890",
"errors": [
{
"field": "email",
"code": "invalid_format",
"message": "Must be a valid email address.",
"rejected_value": "not-an-email"
},
{
"field": "password",
"code": "too_short",
"message": "Must be at least 8 characters."
}
],
"doc_url": "https://api.example.com/docs/errors/validation-error"
}
Field purposes:
| Field | For | Audience |
|---|---|---|
type | RFC 9457 problem type URI. Dereferenceable to docs. | Standards-compliant clients, API gateways |
title | Stable human-readable summary. Never changes between occurrences. | Logging, generic displays |
status | HTTP status code mirrored in body. | Clients that lose the HTTP layer (queues, logs) |
detail | Occurrence-specific explanation. | Developer debugging |
code | Machine-readable stable string. Clients switch on this. | Client error-handling logic |
instance | Request or trace ID. | Support correlation, log lookups |
errors[] | Per-field validation details. | Form UIs highlighting individual fields |
doc_url | Link to detailed error documentation. | Developers integrating your API |
Set the Content-Type header to application/problem+json for RFC 9457 compliance.
Return all validation errors at once, not one at a time. Each entry in errors[] must include the field path, a machine-readable code, and a human-readable message.
For nested objects, use dot notation: billing_address.zip_code. For arrays, use bracket notation: line_items[0].quantity. This mirrors JSON Pointer in a more developer-friendly form.
{
"errors": [
{
"field": "billing_address.zip_code",
"code": "invalid_format",
"message": "Must be a 5-digit ZIP code."
},
{
"field": "line_items[0].quantity",
"code": "out_of_range",
"message": "Must be between 1 and 1000."
}
]
}
Include rejected_value when the value is not sensitive. Omit it for passwords, tokens, and secrets. The code field is for machines -- clients switch on it. The message field is for humans -- clients must never parse or match on message strings.
RFC 9457 (2023, superseding RFC 7807) defines a standard error format for HTTP APIs. Adopt it as your base. The five standard fields are type, title, status, detail, and instance. Extension fields like code, errors[], and doc_url are explicitly allowed and encouraged.
The type URI serves double duty: it is a machine-readable identifier and, when dereferenced, links to human-readable documentation. Default it to "about:blank" if you have no specific error type page.
{
"error": {
"type": "invalid_request_error",
"code": "parameter_missing",
"message": "Missing required param: source.",
"param": "source",
"doc_url": "https://stripe.com/docs/error-codes/parameter-missing"
}
}
Lessons: Hierarchy of specificity (type -> code -> message). The param field maps directly to form fields. Every error links to its documentation page. Errors are wrapped in "error": {} to distinguish from success responses.
{
"message": "Validation Failed",
"errors": [
{ "resource": "Issue", "field": "title", "code": "missing_field" }
],
"documentation_url": "https://docs.github.com/rest/reference/issues"
}
Lessons: Each error identifies the resource, field, and code. Codes are a small, stable enum (missing, missing_field, invalid, already_exists, unprocessable). Documentation URL always present.
{
"code": 21211,
"message": "The 'To' number is not a valid phone number.",
"more_info": "https://www.twilio.com/docs/errors/21211",
"status": 400
}
Lessons: Numeric error codes, each with a dedicated documentation page. Flat structure, one error per response. Simpler but less expressive for batch validation.
Takeaway: All three leaders share the same pattern -- a stable machine-readable code, a human-readable message, and a link to documentation. Adopt all three in your error envelope.
Every error response should include a doc_url that links to a page explaining the error in detail -- causes, solutions, and examples. Stripe, GitHub, and Twilio all do this. The incremental cost of maintaining these pages is low; the reduction in support tickets is significant.
Write message and detail fields as if they will be the only thing a developer reads. Be specific: "The start_date field must be before end_date" beats "Invalid date range." Be actionable: tell the developer what to do, not just what went wrong.
Generate a unique request ID for every incoming request. Return it in a response header (X-Request-Id or Request-Id) and embed it in error responses as the instance field. When a developer files a support ticket, the request ID lets you jump directly to the relevant log entries, traces, and metrics.
For distributed systems, propagate a trace ID across service boundaries and include it alongside the request ID. This turns a vague "the API returned 500" into a precise trail through your infrastructure.
Bad -- 200 with error body:
HTTP/1.1 200 OK
{ "success": false, "error": "User not found" }
Problems: Infrastructure sees success. No machine-readable code. No doc link.
Good -- correct status code, structured envelope:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/resource-not-found",
"title": "Resource Not Found",
"status": 404,
"detail": "No user found with ID 99999.",
"code": "user_not_found",
"instance": "urn:request:a1b2c3d4",
"doc_url": "https://api.example.com/docs/errors/resource-not-found"
}
Bad -- single string for multiple validation errors:
HTTP/1.1 400 Bad Request
{ "error": "Invalid email and password too short" }
Problems: Cannot map to form fields. Client must parse natural language. Rewording breaks clients.
Good -- per-field, coded validation errors:
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/validation-error",
"title": "Validation Error",
"status": 422,
"detail": "2 fields failed validation.",
"code": "validation_error",
"errors": [
{ "field": "email", "code": "invalid_format", "message": "Must be a valid email address." },
{ "field": "password", "code": "too_short", "message": "Must be at least 8 characters." }
],
"doc_url": "https://api.example.com/docs/errors/validation-error"
}
Bad -- 500 with stack trace:
HTTP/1.1 500 Internal Server Error
{ "error": "NullPointerException at UserService.java:142", "stack": "..." }
Good -- safe 500 with trace ID:
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/internal-error",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred. Please try again later.",
"code": "internal_error",
"instance": "urn:request:a1b2c3d4-e5f6-7890"
}
Working implementations in examples/:
examples/error-envelope-implementation.md -- Complete error response system with typed error classes, consistent envelope, per-field validation errors, and request ID injectionexamples/status-code-middleware.md -- Centralized error handler middleware that maps exceptions to correct status codes and formatsWhen designing or reviewing API error responses:
code that is stable and documentedmessage or detail for developersRetry-After header and rate limit contextdoc_url or dereferenceable type URI linking to documentationContent-Type is set to application/problem+json for error responses