APIs Are Forever

When you write an internal function, you can refactor it tomorrow. When you publish an API that three other services depend on, breaking changes become an incident.

That asymmetry is why API design deserves more upfront thought than almost any other engineering decision. The patterns below have saved us from countless painful migrations.

Pattern 1: Version from Day One

Even if you're "just prototyping," put a version in your URL:

/api/v1/users
/api/v1/organizations

When you need to make a breaking change, you add /v2 without touching /v1. Consumers migrate on their own schedule. You don't wake up at 3am because a deploy broke an integration you forgot existed.

Pattern 2: Return Envelopes, Not Raw Data

Don't return a bare array:

[{ "id": 1, "name": "Alice" }, { "id": 2, "name": "Bob" }]

Return an envelope with metadata:

{
  "data": [
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" }
  ],
  "meta": {
    "total": 142,
    "page": 1,
    "perPage": 20
  }
}

This gives you room to add pagination, cursor info, and other metadata without breaking existing clients. A bare array has nowhere to grow.

Pattern 3: Use Consistent Error Shapes

Pick a shape and use it everywhere:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Email address is already in use.",
    "field": "email"
  }
}

Clients should be able to write a single error handler that works for every endpoint. If your 400 response and your 404 response look different, you're making every API consumer write special cases.

Machine-readable error codes (VALIDATION_ERROR, NOT_FOUND, RATE_LIMITED) are more useful than HTTP status codes alone. The status code tells you the category; the code tells you exactly what happened.

Pattern 4: Pagination as a First-Class Concern

There are two sane pagination strategies: offset-based and cursor-based.

Offset pagination (?page=2&perPage=20) is simple but breaks under concurrent writes because the "second page" shifts while the user is reading the first.

Cursor pagination (?after=eyJpZCI6MjB9) is more complex but stable. Use it for any feed or timeline that mutates frequently.

Whichever you choose: never return an unbounded list. Always paginate. An endpoint that returns all users works fine with 100 users and causes a 30-second timeout with 100,000.

Pattern 5: Idempotency Keys for Mutations

For expensive or side-effectful operations (payments, email sends, deployments), accept an idempotency key:

POST /api/v1/charges
Idempotency-Key: 7f2c8a1e-4b3d-4f9e-b1a2-3c4d5e6f7a8b

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

If the client retries due to a timeout, the server detects the duplicate key and returns the original response without processing the charge twice.

This is the difference between "charge failed, we'll retry" and "charged twice, customer filed a dispute."

Pattern 6: Design for Your Worst Client

Your best client is a well-behaved internal service that makes exactly one request per second and handles errors gracefully.

Your worst client is a mobile app on a spotty LTE connection that retries aggressively, doesn't handle 429s, and sends malformed requests on older OS versions.

Design for the worst client. Rate limit. Return clear errors. Be lenient on input parsing. Your best client won't be affected; your worst client will actually work.

The Thing That Matters Most

All of these patterns exist to serve one goal: making your API boring to consume. The API that nobody complains about is the one where everything works the way you expect, every time, without having to read the documentation twice.

That's the bar. Boring is the goal.