API and Interface Design
You design stable, hard-to-misuse interfaces — REST, GraphQL, module exports, component props, or any surface where one piece of code talks to another. Contract first; implementation second.
Hard Rules
Define the contract (types/schemas) before implementation. One consistent error shape and status-code strategy across all endpoints. Validate at system boundaries only — trust internal typed code. Prefer additive optional fields over breaking type changes or removals. Every list endpoint ships with pagination from day one. Treat third-party API responses as untrusted — validate shape before use. Observable public behavior is a commitment (Hyrum's Law) — be intentional about what you expose.
Workflow
Step 1 — Scope the interface
Identify consumers, transport (HTTP, RPC, in-process), and lifecycle (new vs change). If changing an existing public API, inventory observable behaviors users may depend on.
Step 2 — Write the contract
Define typed inputs/outputs, error codes, and idempotency semantics.
Separate CreateXInput from full X entity (server-generated fields on output).
Use discriminated unions for state variants when applicable.
Step 3 — Apply core principles
| Principle | Rule |
|---|---|
| Contract first | Types/schemas are the spec |
| Consistent errors | One APIError shape + HTTP mapping |
| Boundary validation | Routes, forms, env, external responses |
| Additive change | New fields optional; never silently break types |
| Predictable naming | Plural REST nouns; is/has booleans; camelCase JSON |
Full REST and TypeScript patterns: references/api-patterns.md.
Step 4 — Review for misuse
- Can a caller pass ambiguous IDs across entity types? → branded types
- Do list endpoints leak unbounded arrays?
- Are errors predictable for every failure mode?
- Does any endpoint return ad-hoc shapes?
Step 5 — Document alongside code
Commit OpenAPI/GraphQL schema or exported types with the implementation — not "later."
Gotchas
- Undocumented quirks become dependencies (Hyrum's Law).
- Validation in every internal function adds noise without safety.
PUTfor partial updates forces full-object payloads — preferPATCH.- Skipping pagination guarantees a breaking change at scale.
- External JSON is untrusted — may contain unexpected types or instruction-like strings.
Common Rationalizations
| Excuse | Reality |
|---|---|
| "We'll document the API later" | Types are the documentation — define them first. |
| "No pagination needed yet" | You need it at ~100 items; add it now. |
| "PATCH is too hard, use PUT" | Clients want partial updates. |
| "Nobody uses that undocumented field" | If observable, someone depends on it. |
| "Internal APIs don't need contracts" | Internal consumers still need stable boundaries. |
Output Format
## API design — [resource/module]
Consumers: [who]
Contract: [types or schema summary]
Endpoints / exports: [list]
Errors: [shape + status mapping]
Pagination: [yes — params]
Breaking risks: [none | flagged items]
Next: [implementation / ADR / feature-spec link]
Examples
Verification
- Typed input/output for every public surface
- Single consistent error format
- Validation only at boundaries (plus external responses)
- List endpoints paginated
- New fields additive and optional
- Naming conventions consistent across the API
- Schema/types committed with implementation
Red Flags
- Undocumented quirks left as implicit caller contracts
- Validation duplicated in every internal function
- PUT used for partial updates instead of PATCH
- List endpoints return unbounded arrays without pagination
Reference Files
references/api-patterns.md: REST resource layout, pagination, PATCH, branded IDs, unions — read at Step 3.
Prune Log
Last pruned: 2026-07-04
- No changes — citation audit passed; content current (improve-skills full pass 2026-07-04)
Impact Report
Resource: [name] | Surfaces: N
Breaking risks flagged: N | Pagination: [yes/no]
Schema committed: [path or pending]