
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
emailfield - 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 Requestswhen 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
400for 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
429with 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.