API · Design

API Design Principles — A Field Guide

API Design Principles — A Field Guide is the work that makes the systems talk. The API is the contract between the producer and the consumer; the contract is what determines.

John Kihiu12 min read

Good API design is mostly about picking conventions once and applying them everywhere, so a consumer who's learned one endpoint can guess the shape of the next one. The principles below aren't novel — they're the ones that show up, in some form, in every API that's still pleasant to integrate against five years after launch.

Model resources as nouns, not RPC-style actions

A REST-shaped API names endpoints after the things it manages (/orders, /orders/{id}/line-items) and expresses actions through HTTP methods, not verbs in the URL. POST /orders/{id}/cancel is a common and reasonable exception for actions that don't map cleanly to CRUD — but /getOrderById or /updateOrderStatus as separate endpoints signals the API is really RPC wearing REST's clothing, which is fine as a deliberate choice (gRPC, tRPC) but confusing as an accident.

Idempotency keys make retries safe

Network failures mean a client can never be certain whether a POST request that timed out actually succeeded server-side. An Idempotency-Key header (a client-generated UUID, per request) lets the server recognize a retried request and return the original result instead of creating a duplicate — this is the pattern Stripe popularized and it's now close to expected on any payment or order-creation endpoint. Without it, "just retry on timeout" is not a safe instruction to give API consumers.

HTTP · IDEMPOTENT POST
POST /v1/charges HTTP/1.1
Idempotency-Key: 7d8f2a91-4c3e-4b1a-9e5d-2f8c1a6b3d4e
Content-Type: application/json

{"amount": 2000, "currency": "usd", "customer": "cus_9s6XKzkNRiz8i3"}

# A retried request with the same key returns the original
# response (same status, same body) instead of double-charging.

Cursor pagination over offset for anything that grows

Offset-based pagination (?page=3&limit=20) breaks under concurrent writes — an insert or delete between page fetches shifts every subsequent page's contents, causing skipped or duplicated records. Cursor-based pagination (?after=eyJpZCI6MTIzfQ, an opaque token derived from the last item's sort key) stays consistent regardless of concurrent writes and is what every high-traffic API — Stripe, GitHub, Slack — settled on for exactly this reason.

Always return the next cursor, never make clients construct one

The pagination response should include the exact cursor value for the next page ("next_cursor": "eyJpZCI6MTQz...") rather than expecting the client to derive it from the last item. Treating the cursor as opaque lets you change its internal encoding later without breaking every client that reverse-engineered its structure.

Error responses need the same design rigor as success responses

A consistent error shape — machine-readable error code, human-readable message, and where relevant a field-level breakdown for validation errors — turns error handling from special-cased guesswork into something a client can branch on programmatically. RFC 9457 (Problem Details for HTTP APIs) standardizes this shape (type, title, status, detail, instance) specifically so clients don't need bespoke parsing logic per API.

Don't overload HTTP status codes to carry business logic

Returning 200 with an {"success": false} body, or conversely inventing non-standard status codes for business rule violations, forces every client to special-case your API instead of relying on standard HTTP semantics. Use the status code for what actually happened at the protocol level (404 for not found, 422 for validation failure, 409 for conflict) and put business detail in the body.

Design for additive change from day one

Clients that ignore unknown JSON fields (the common, recommended default) let you add fields without a version bump. The discipline this requires: never repurpose an existing field's meaning, never remove a field without a formal deprecation cycle, and default new optional fields to a value that preserves old behavior. APIs that get this right rarely need a v2; APIs that don't end up maintaining parallel major versions far sooner than planned.

PrincipleWhy it matters
Nouns for resources, HTTP methods for actionsPredictable endpoint shape across the whole API
Idempotency keys on unsafe writesMakes client-side retry safe by default
Cursor paginationCorrect under concurrent writes, unlike offset
RFC 9457 error shapeOne error-parsing code path for every client

Wrapping up

None of these principles are exotic — idempotency keys, cursor pagination, and structured errors are all well-documented, widely adopted patterns. The value is in applying them consistently across an entire API surface rather than case by case, so a consumer's mental model built from one endpoint transfers cleanly to the next.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.