API error taxonomy: from cleanup request to contract spec

This case shows how a vague "improve API errors" request becomes a stable contract that SDKs, AI-generated clients, and reviewers can depend on.

RiskClient compatibility and retries
BoundaryEnvelope only, no endpoint behavior change
EvidenceFixtures, SDK sample, OpenAPI examples

The request before the spec

Weak ticket

Improve API errors.
Make them easier for clients to handle.

Spec-first rewrite

Feature: Stable API error taxonomy
Owner: API Platform
Status: Draft for review

Goal:
- Standardize error code, category, message, trace_id, and retryable.
- Keep existing HTTP status behavior.
- Help generated clients branch without string parsing.

Non-goals:
- No endpoint business-logic changes.
- No removal of legacy fields during the first release.
- No new auth policy.

The contract that stops hidden breakage

error-envelope.md

{
  "error": {
    "code": "ORDER_ALREADY_EXISTS",
    "category": "conflict",
    "message": "An order already exists for this idempotency key.",
    "trace_id": "req_01H...",
    "retryable": false,
    "details": {}
  }
}

acceptance-criteria.md

- Given validation fails
  When the API returns 422
  Then error.category is validation and details.field_errors exists.

- Given a duplicate idempotency key
  When the API returns 409
  Then error.retryable is false and code is stable.

compatibility.md

- Keep legacy top-level message for one release.
- Add new envelope fields behind additive response change.
- Publish SDK fixture before turning examples into docs.
- Log unknown categories during migration.

test-evidence.md

Automated:
- 400/401/409/422 contract fixtures
- SDK parser compatibility test
- OpenAPI example snapshot

Manual:
- release note reviewed by client support
- logs show category and trace_id

Why this belongs in a spec packet

It separates shape from behavior

The spec limits the change to the error envelope so implementation cannot drift into endpoint semantics.

It protects generated clients

Stable categories and retryable flags give AI-generated SDKs a machine-readable branch point.

It creates migration evidence

Fixtures, examples, and logs make compatibility visible before rollout, not after support tickets arrive.

Reviewer walkthrough

The key review move is to treat error payloads as public product behavior. A vague cleanup request lets implementation rename fields, collapse categories, or change retry behavior because the change "only affects errors." A spec packet makes the envelope a contract: additive fields are allowed, old parsing behavior is preserved for a migration window, and every status code has a fixture.

The strongest part of this example is the compatibility file. It prevents the team from publishing new examples before SDKs can parse them, and it gives support a release note that matches the actual response shape. That is especially important when users copy API examples into AI assistants; stale examples can become generated client bugs weeks later.

Shape check

Every response should keep the same envelope keys, stable categories, a trace identifier, and a clear retryable flag. Client code should not need to parse English messages.

Compatibility check

The first release should keep legacy fields, publish SDK fixtures, and log unknown categories before removing old examples or changing docs.

Evidence check

Reviewers should see contract tests for the common status codes, an OpenAPI example snapshot, and at least one generated-client parser sample.

How to adapt this case

Use this case for any public response shape that developers may copy into code: REST errors, webhook payloads, GraphQL errors, SDK exceptions, CLI output, or generated client examples. The exact fields can change, but the migration discipline should stay the same. Add fields before removing old ones, publish fixtures before rewriting docs, and give consumers a way to detect retry behavior without reading prose.

The spec should also name who consumes the contract. Internal frontend code, external SDKs, integration partners, support tooling, and AI assistants may all learn from different examples. If those examples drift, implementation bugs appear in places the API team never directly touched. That is why the evidence list includes SDK parsing and OpenAPI snapshots, not only server tests.

When reviewing an AI-generated implementation, be strict about accidental behavior changes. A refactor that changes status codes, message wording, or retry flags may look harmless in a diff, but it can break client control flow. Treat those changes as contract changes and send them back to the spec before they reach code.

Anti-patterns to reject

These anti-patterns are included because they are easy for both humans and AI tools to miss during a cleanup. They do not always break tests immediately, but they create client drift that appears later in SDKs, examples, and copied integration code. A useful review should catch that drift while the response shape is still a proposal, not after customers have copied the new examples. That timing is the difference between contract design and support cleanup.

Message-only branching

Clients should not decide retry behavior by reading an English sentence. Give them a stable code, category, and retry flag.

Docs before fixtures

Do not update examples before the parser fixtures and OpenAPI snapshots prove the new shape is stable.

Silent field removal

Removing legacy fields without a migration window turns a cleanup project into a breaking change.

What to watch after launch

An API error spec is not finished when the pull request merges. The real validation is whether clients, docs, SDKs, and support workflows all start using the same taxonomy.

Unknown category logs

Keep a short-term query for unknown categories so you can catch legacy SDKs falling into default branches before customers report it.

Example drift

Recheck OpenAPI examples, quickstart snippets, and SDK READMEs after release. Copied stale examples become generated-client bugs.

Use this pattern before changing API responses

Generate a packet first, then pair it with the API contract checklist so every response example has evidence.

Editorial note

This case focuses on API contract safety: error formats are treated as public behavior, not as internal cleanup.