Stop Designing APIs for Yourself. Design Them for the Person Calling Them.
by Eric Hanson, Backend Developer at Clean Systems Consulting
The API makes sense—to the people who built it
You can usually tell who an API was designed for by how easy it is to use without context.
If it was designed for backend engineers:
- endpoints mirror database tables
- responses expose internal IDs and join structures
- workflows require multiple round trips and orchestration
If it was designed for consumers:
- endpoints map to real use cases
- responses contain everything needed for the task
- common workflows are simple and predictable
Most APIs fall into the first category. Not because teams don’t care, but because designing for yourself is the default.
You already understand the system. Your consumers don’t.
The symptom: clients doing the heavy lifting
A dead giveaway that your API is self-centered:
GET /users/{id}
GET /orders?user_id={id}
GET /order_items?order_id={orderId}
This forces the client to:
- understand relationships between entities
- orchestrate multiple requests
- handle partial failures across calls
That’s not just inconvenient. It creates duplication across every client.
Better:
GET /users/{id}/orders?include=items
Now the server handles aggregation. The client gets a complete view aligned with its needs.
Yes, it’s more work on the backend. That’s the point.
Think in workflows, not endpoints
Consumers don’t think in terms of endpoints. They think in terms of tasks:
- “Show a user their recent orders”
- “Cancel an order”
- “Create a checkout session”
If your API forces them to map tasks to multiple low-level calls, you’ve shifted complexity in the wrong direction.
Example: canceling an order.
Backend-centric design:
GET /orders/{id}
PATCH /orders/{id} { "status": "cancelled" }
POST /refunds
Consumer-centric design:
POST /orders/{id}/cancel
This is one place where strict REST purity often loses to usability. Modeling everything as state transitions can make simple workflows harder than they need to be.
Be pragmatic. The goal is not ideological correctness. It’s reducing friction for the caller.
Design responses to eliminate guesswork
Another common issue: responses that technically contain the data, but require interpretation.
Example:
{
"status": "2",
"flag": true
}
What does 2 mean? What does flag represent?
Now compare:
{
"status": "shipped",
"is_expedited": true
}
The second version removes ambiguity.
Even better, include context where useful:
{
"status": "shipped",
"estimated_delivery": "2026-04-21",
"can_cancel": false
}
Now the client doesn’t need to infer business rules.
You’re not just returning data. You’re communicating intent.
Reduce the number of decisions the client has to make
Every optional parameter, every conditional field, every branching behavior is a decision pushed to the client.
Example:
GET /orders?include_items=true&include_payments=true&include_shipping=true
This looks flexible, but it forces the client to understand what combinations make sense.
A more consumer-friendly approach:
GET /orders/{id}/summary
or:
GET /orders/{id}?view=full
You define the shapes that matter. Clients pick from known-good options.
Flexibility is useful, but too much of it shifts responsibility outward.
Error messages should guide, not just report
If an API is designed for its consumers, errors help them recover.
Bad:
{
"error": "Invalid request"
}
Better:
{
"error": {
"code": "INVALID_STATUS",
"message": "Status must be one of: pending, shipped, cancelled",
"details": {
"field": "status"
}
}
}
Now the client can:
- display meaningful feedback
- correct the issue programmatically
- avoid guesswork
This reduces support overhead and speeds up integration.
Internal models are not your API
One of the hardest habits to break: exposing internal structures directly.
Example:
{
"user_id": "123",
"order_ids": ["a", "b", "c"]
}
This forces the client to fetch more data.
Instead:
{
"user": {
"id": "123",
"orders": [
{ "id": "a", "total": 100 },
{ "id": "b", "total": 50 }
]
}
}
Yes, this duplicates some data across endpoints. That’s fine.
Your API is not a normalized database. It’s an interface optimized for consumption.
The tradeoff: backend complexity vs client simplicity
Designing for the caller has a cost:
- more complex query logic (joins, aggregations)
- heavier responses (larger payloads)
- more opinionated endpoints (less raw flexibility)
But pushing that complexity to clients is worse:
- every consumer reimplements the same logic
- inconsistencies multiply across clients
- bugs become harder to track
You want complexity in one place—the backend—where you control it.
A practical way to shift perspective
When designing or reviewing an endpoint, ask:
- How many requests does a client need to complete a common task?
- What assumptions does the client have to make?
- What errors are ambiguous or hard to recover from?
- Would someone unfamiliar with the system use this correctly on the first try?
If the answers are uncomfortable, the API is still designed for you.
What to do differently this week
Pick a common client workflow in your system.
Trace every API call required to complete it.
Then ask: how many of those calls could be collapsed into one?
Implement that as a new endpoint or improve an existing one.
You don’t need to redesign everything. Just remove one point of friction.
That’s how APIs start feeling intentional instead of accidental.