← Recipes

阅读中文版本

Audit API Error Response Shapes for Consistency

Collect real 4xx and 5xx responses across endpoints, normalize them into a report of shapes and fields, and converge everyone onto one error envelope.

~40 minapidesigntesting

Task

Harvest error responses from every API endpoint, classify their shapes, and produce the report that drives a single consistent error schema.

When to use this

Client SDKs contain a switch with twelve branches because validation errors arrive as message strings, arrays, and nested detail objects depending on which team owns the endpoint.

Prerequisites

  • A way to trigger representative errors on every endpoint, such as a smoke script
  • A script or tool to record full response bodies
  • Buy-in from endpoint owners to converge on one schema

Steps

1. Trigger one validation error per endpoint and record bodies.

for ep in /orders /users /payments /search; do curl -s -X POST "https://api.example.com$ep" -H "content-type: application/json" -d '{}' | jq -c "{ep: \"$ep\", body: .}"; done

Expected output: One line per endpoint shows each error body side by side, and the shape differences are immediately visible.

2. Classify the observed shapes mechanically.

curl -s -X POST https://api.example.com/orders -d '{}' -H "content-type: application/json" | jq 'if has("error") then "envelope" elif has("message") then "flat" elif has("errors") then "rails-style" else "other" end'

Expected output: Each endpoint falls into a shape class: envelope, flat message, errors array, or unclassified.

3. Check which fields clients actually need.

grep -rhoE "\.(error|message|detail[s]?|code)\b" clients/sdk/src | sort | uniq -c

Expected output: The SDK field usage count shows which members the de facto standard already is, usually code and message.

4. Define the target envelope and a compatibility mapping.

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Request body is missing required fields.",
    "details": [{"field": "quantity", "issue": "must be >= 1"}]
  }
}

Expected output: The proposed schema covers every observed shape via mapping: message strings become message, arrays become details, codes become code.

5. Add a conformance test that enforces the envelope.

it("returns the standard error envelope", async () => {
  const res = await request(app).post("/orders").send({});
  expect(res.status).toBe(422);
  expect(res.body.error).toMatchObject({ code: expect.any(String), message: expect.any(String) });
});

Expected output: New endpoints failing the envelope shape break the build, so drift cannot reaccumulate silently.

Troubleshooting

Problem

Some errors are plain text or HTML

Solution

Error middleware is missing on those routers; route all failures through one handler that renders the JSON envelope and sets the correct content type.

Problem

Clients depend on legacy shapes

Solution

Version the change or dual-emit the new envelope plus legacy fields for a deprecation window, with a sunset date communicated to consumers.

Problem

5xx bodies leak stack traces

Solution

The envelope for server errors must carry only an id clients can quote; log the detail server-side and return a correlation id.

Limitations

  • The audit sees triggered errors only; untested branches may still deviate, so keep the conformance test broad rather than sampling.
  • A shared envelope standardizes shape, not semantics; a VALIDATION_FAILED code still needs documented per-endpoint meanings.

Error responses are an API face too

Teams polish success payloads and improvise failures, so clients grow twelve-branch error parsing and every new endpoint adds a thirteenth. The audit is mechanical: trigger a validation error everywhere, record the bodies, and classify the shapes. The report makes divergence undeniable, the SDK usage count picks the de facto winner, and one envelope with a conformance test keeps the standard from eroding again.

# Trigger and compare error bodies across endpoints
for ep in /orders /users /payments /search; do
  curl -s -X POST "https://api.example.com$ep" \
    -H "content-type: application/json" -d '{}' | jq -c "{ep: \"$ep\", body: .}"
done

Let usage elect the standard

Do not design the envelope in a vacuum. Count which fields the existing SDKs already read; that count is an election with votes cast in production code. Adopting the winning shape minimizes client migration, and the mapping table documents exactly how each legacy shape translates into the new one.

Guard the standard with a test, not a document

Style guides drift because nothing enforces them. A conformance test that posts invalid payloads to every route and asserts the envelope shape converts the guideline into a build failure. New endpoints inherit the standard automatically, and the occasional violation is caught in the pull request that introduces it.