Skip to content
All postsDec 17, 2024 · 5 min read

API Design: Why Designing Beats Building Every Time

Any developer can make an endpoint return data. Designing one that other developers can guess, debug and depend on for years is a different skill. Here's what separates the two.

Building an API takes an afternoon.

Set up a route. Connect it to a controller. Return some data.

It works. You ship it.

Then another team starts using it. They guess endpoint names and get them wrong. They get 400 Bad Request for ten different problems. You change a field name and their app breaks.

The API works. It just isn't designed.

Building focuses on functionality. Designing focuses on the people who call your API: is it predictable, easy to debug, and safe to change?

Here's what that looks like in practice.

Building vs. Designing an API

A built API answers one question: does it return the right data?

A designed API also answers:

  • Can a developer guess the endpoint without reading the docs?
  • Does the response look the same every time?
  • When something fails, does the error explain what to fix?
  • Can you change it next year without breaking everyone?

Good design isn't decoration. It decides whether people can use your API without messaging you every day.

Predictability Over Cleverness

Your endpoints should be so consistent that developers can guess them.

  • Bad: /fetch_users, /getUsers, /obtainUserData
  • Good: /users, /users/{id}

Two rules carry most of the weight:

  • Use nouns, not verbs. The HTTP method is the verb.
  • Follow the conventions developers already know. If every API they've used has /users, yours should too.

Use Descriptive Endpoint Names

Use plural nouns for collections and an ID for a single item:

Action Request
List users GET /users
Get one user GET /users/{id}
Create a user POST /users
Replace a user PUT /users/{id}
Delete a user DELETE /users/{id}

This is the foundation of REST. It's simple, and it removes a whole class of questions.

Use the Right HTTP Status Codes

A well-designed API communicates through status codes. If every error returns 400, the client has to parse your message text to know what happened.

Code Meaning
200 OK Success, the body contains data
201 Created A new resource was created
204 No Content Success, nothing to return
400 Bad Request The client sent invalid data
401 Unauthorized The client isn't authenticated
403 Forbidden Authenticated, but not allowed to do this
404 Not Found The resource doesn't exist
429 Too Many Requests The client hit a rate limit
500 Internal Server Error Something broke on the server

Write Errors That Explain the Fix

Nothing slows a developer down like "Something went wrong."

A useful error says three things:

  • What failed: the email field
  • Why it failed: it isn't a valid address
  • How to fix it: send it in the format user@example.com

Instead of this:

{
  "code": 400,
  "message": "Invalid parameter"
}

Send this:

{
  "error": "invalid_email",
  "message": "The 'email' field must be a valid email address, like user@example.com.",
  "field": "email"
}

A stable, machine-readable error code (invalid_email) lets clients handle the error in code and lets support look it up, even if you reword the message later.

Version Before You Need To

APIs change. If you don't version yours, every breaking change breaks your users.

Two common approaches:

  • In the URL: /v1/users, /v2/users
  • In a header: X-API-Version: 1

When a change isn't backward-compatible, release it as a new version and give clients time to move.

Rate-Limit to Protect Your System

One client in a retry loop can flood your API, slow it down for everyone, and raise your bill.

  • Set a limit, like 100 requests per minute per client.
  • Return 429 Too Many Requests when it's hit.
  • Tell clients where they stand with headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 10
X-RateLimit-Reset: 1671403955

Now clients know how many requests they have left and when the window resets.

Paginate Large Results

If one request returns 10,000 records, it will eventually time out.

Offset pagination uses offset and limit:

GET /users?offset=0&limit=50

It's simple, but it gets slow on large tables, and rows can shift between pages while someone is paging.

Cursor pagination returns a pointer to where the last page ended:

GET /users?limit=50&cursor=abc123

It stays fast at any size and doesn't skip or repeat rows. It's harder to build, and you can't jump to page 37.

Use cursors for large or fast-changing datasets. Offsets are fine for small admin tables.

Key Takeaways

  • Use nouns and plural collections: /users, not /fetchUsers.
  • Return the status code that matches what happened, not 400 for everything.
  • Make errors say what failed, why, and how to fix it, with a stable error code.
  • Version your API before the first breaking change.
  • Rate-limit every public endpoint and return 429 with limit headers.
  • Paginate. Prefer cursors for large datasets.

Final Thoughts

An API is a promise to everyone who calls it.

Building makes the promise work today. Designing makes it something people can still rely on a year from now.