A working Express API that demonstrates production-ready error response patterns. Built as a companion to a workshop / blog post on designing errors that developers (and AI agents) can actually use.
npm install
npm startThe server starts on http://localhost:3000 (override with PORT env var).
| Pattern | Endpoint | What You'll See |
|---|---|---|
| Error catalog | GET /errors |
Machine-readable list of every error code |
| Auth errors | POST /webhooks (no key) |
Distinct codes for missing vs invalid keys |
| Field-level validation | POST /webhooks (bad body) |
Per-field errors with rejected values and hints |
| Optimistic locking | PATCH /configs/:id |
Version conflict with change history |
| Downstream failure | POST /documents/verify |
Transient upstream timeout with retry guidance |
| Partial batch failure | POST /messages/batch |
HTTP 207 with per-item success/failure |
| Rate limiting | Any authed route (11+ calls) | 429 with machine-readable retry fields |
Use either of the demo keys:
Authorization: Bearer sk-live-demo
Authorization: Bearer sk-test-demo
Browse the error catalog:
curl -s http://localhost:3000/errors | jqMissing auth (401 with AUTH_MISSING code):
curl -s http://localhost:3000/webhooks -X POST | jqValidation errors with per-field detail:
curl -s http://localhost:3000/webhooks -X POST \
-H "Authorization: Bearer sk-live-demo" \
-H "Content-Type: application/json" \
-d '{"url":"http://myapp.com/hook","events":["fake.event"]}' | jqVersion conflict (optimistic locking):
# First, GET the current config to see the real ETag
curl -s http://localhost:3000/configs/cfg_001 \
-H "Authorization: Bearer sk-live-demo" | jq
# Then PATCH with a stale ETag to trigger a 412 conflict
curl -s http://localhost:3000/configs/cfg_001 -X PATCH \
-H "Authorization: Bearer sk-live-demo" \
-H "Content-Type: application/json" \
-H 'If-Match: "etag-v2"' \
-d '{"theme":"dark"}' | jqDownstream timeout (70% chance, for demo purposes):
curl -s http://localhost:3000/documents/verify -X POST \
-H "Authorization: Bearer sk-live-demo" | jqPartial batch failure (HTTP 207):
curl -s http://localhost:3000/messages/batch -X POST \
-H "Authorization: Bearer sk-live-demo" \
-H "Content-Type: application/json" \
-d '{"messages":[{"to":"+44700000001","body":"Hello"},{"to":"not-a-number","body":"Hi"}]}' | jqRate limiting (send 11+ requests quickly):
for i in $(seq 1 12); do
curl -s http://localhost:3000/webhooks -X POST \
-H "Authorization: Bearer sk-live-demo" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/hook","events":["order.created"]}' | jq .error.code
doneEvery error follows the same structure:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Request body failed validation.",
"hint": "Check the 'errors' array for specific fields that need fixing.",
"docs_url": "http://localhost:3000/errors/VALIDATION_FAILED"
},
"request_id": "req_a1b2c3d4e5f6g7h8"
}Key properties:
code-- Stable, machine-readable string (never changes)message-- Human-readable explanation of what went wronghint-- Actionable next step to fix the problemdocs_url-- Link to full documentation for this error coderequest_id-- Unique ID for log correlation and support tickets
Endpoints may add extra fields in the error object (e.g. errors[] for validation, retry_after_seconds for rate limits) -- the shape is always additive, never breaking.
server.ts -- The entire API in one file, heavily commented (TypeScript)
Written in TypeScript, executed directly with tsx -- no build step needed.
Sections in server.ts:
- Error Catalog -- Single source of truth for all error codes
- ApiError Class -- Structured error that flows from origin to response
- Request ID Middleware -- Assigns
X-Request-Idto every request - Auth Middleware -- Demonstrates missing vs invalid key errors
- Rate Limiter -- In-memory limiter with machine-readable headers
- Routes -- Five endpoints showcasing different error patterns
- Error Handler -- Formats
ApiErrorinstances, catches unexpected errors
MIT