# API Design Principles: A Practical Guide — Routebase

> The six design decisions that make or break an API: resource modeling, naming, idempotency, pagination, error formats, and versioning — with concrete examples.

Canonical page: https://routebase.dev/blog/api-design-principles/
Published: 2026-07-07 · The Routebase Team · API Design, REST, Guide

Most APIs don't fail because of technology choices. They fail because of a
hundred small design decisions made inconsistently, under deadline pressure,
by different people who never agreed on the rules. The result is an API that
works but fights its consumers: three different pagination styles, error
responses that change shape between endpoints, and a `PUT` that isn't safe to
retry.

This guide covers the six decisions that matter most. None of them are new —
that's the point. Good API design is mostly about picking a convention and
applying it everywhere.

## 1. Model resources, not procedures

The single most common design mistake is exposing your implementation instead
of your domain. An endpoint like `POST /runSyncJob?mode=full` tells consumers
how your system works internally — and breaks the moment that internal changes.

Model the things your consumers talk about, and let HTTP verbs carry the
action:

```http
POST   /projects/42/sync-jobs        # start a sync
GET    /projects/42/sync-jobs/7      # check its status
DELETE /projects/42/sync-jobs/7      # cancel it
```

The test: could a consumer guess the endpoint for a new operation without
reading your docs? If creating a project is `POST /projects`, then creating an
environment had better be `POST /environments` (or
`POST /projects/{id}/environments`) — not `POST /createEnv`.

Nesting deserves restraint. One level (`/projects/{id}/environments`)
expresses ownership; three levels deep, URLs become brittle and clients end up
hardcoding IDs they shouldn't know. When a resource has its own identity,
give it a top-level collection and link to the parent by ID.

## 2. Be boringly consistent with names

Every naming argument has more than one defensible answer — plural vs.
singular collections, camelCase vs. snake_case fields, `createdAt` vs.
`created_at`. What's not defensible is mixing them. Pick once, write it down,
enforce it:

- **Collections are plural nouns:** `/projects`, `/api-specs`, `/test-runs`.
- **Fields use one case, everywhere.** If your JSON is camelCase, your query
  parameters should be too.
- **The same concept keeps the same name across endpoints.** If it's `status`
  on one resource, it isn't `state` on the next.
- **Timestamps are ISO 8601 in UTC,** suffixed consistently: `createdAt`,
  `updatedAt`, `deletedAt`.

This sounds trivial. It's not: naming drift is the earliest and most visible
form of [contract drift](/api-testing/), and it's the one your consumers
notice first — usually in a code review, asking why your SDK has both
`userId` and `user_id`.

_Figure: The test for principles one and two is whether a consumer who has seen three endpoints can reach a fourth without opening the docs. Where one convention was applied throughout, the first guess lands. Where the API grew one reasonable decision at a time — a verb in the path here, a singular there, a case change somewhere else — every guess is a 404, because there is no pattern to generalise from, and the consumer ends up in the docs for a call they should have been able to write blind._

## 3. Make retries safe with idempotency

Networks fail mid-request. When they do, clients retry — and if your API isn't
designed for that, retries create duplicate orders, double charges, and
support tickets.

The HTTP verbs already define the contract: `GET`, `PUT`, and `DELETE` are
idempotent by definition; repeating them must produce the same result. The
hard case is `POST`. For creates that must not double-execute, accept an
idempotency key:

```http
POST /orders
Idempotency-Key: 8e03978e-40d5-43e8-bc93-6894a57f9324
Content-Type: application/json

{ "projectId": "42", "plan": "team" }
```

The server stores the key with the first response and replays that response
for any retry carrying the same key. Consumers can then retry fearlessly —
which means their timeout handling gets simpler, which means fewer bugs that
look like *your* API's fault.

_Figure: The client's behavior is identical on both rows and outside your control: the first response is lost in transit, so it retries twice. Without a key, the server sees three separate intents and creates three orders. With the Idempotency-Key from the request above, it recognises the same request each time — three attempts, one order._

## 4. Paginate from day one

Every collection endpoint will eventually return more data than anyone wants
in one response. Retrofitting pagination is a breaking change; shipping it on
day one is a few lines of design.

Cursor-based pagination is the safer default — offsets skip or duplicate rows
when the underlying data changes between pages:

```json
{
  "data": [ ... ],
  "pagination": {
    "nextCursor": "eyJpZCI6MTAyNH0",
    "hasMore": true
  }
}
```

Whatever you choose, choose it once. A response envelope that's identical
across every list endpoint means consumers write their pagination loop one
time and reuse it everywhere.

## 5. Return errors machines can read

Error handling is API design's most neglected surface. A `400` with
`"something went wrong"` forces every consumer to parse prose. Use a
structured, uniform error shape — [RFC 9457 Problem
Details](https://www.rfc-editor.org/rfc/rfc9457) is the standard worth
adopting:

```json
{
  "type": "https://api.example.com/errors/validation",
  "title": "Validation failed",
  "status": 400,
  "detail": "The 'email' field is not a valid address.",
  "errors": [
    { "field": "email", "message": "must be a valid email address" }
  ]
}
```

Two rules make error design pay off: the same error shape on every endpoint,
and error `type`s that are stable identifiers consumers can branch on —
never strings they have to fuzzy-match. The full treatment — extension
members, type registries, and what must stay out of error bodies — is in
[Consistent Error Handling with RFC 9457 Problem
Details](/blog/consistent-error-handling-rfc-9457/).

## 6. Version deliberately, break rarely

Versioning is where design meets governance. The mechanics (URL prefix,
header, media type) matter less than the policy: know what counts as a
breaking change, additive-change your way around most of them, and when a
break is unavoidable, give consumers a version boundary and a deprecation
window.

That's a topic with enough depth for its own post — see
[Versioning APIs Without Breaking Your Consumers](/blog/api-versioning-without-breaking-consumers/).

## Principles need enforcement

Everything above fits on one page, and that's exactly the problem: knowing
the principles was never the hard part. The hard part is holding a hundred
endpoints to them, across teams, releases, and reviewer moods.

That's an argument for putting the API contract — the spec — at the center of
the workflow instead of leaving design decisions buried in code. When the
spec is the source of truth, conventions become lintable, drift becomes
detectable, and review happens where design happens. That workflow shift is
what [design-first development](/blog/design-first-vs-code-first/) is about —
and building it is what we do at [Routebase](/api-design/).

---

[Routebase](https://routebase.dev/) — [Sign up](https://app.routebase.dev/): Every account starts with a 14-day Pro trial — no credit card required.
