A Good API Is One Developers Never Have to Ask Questions About
by Eric Hanson, Backend Developer at Clean Systems Consulting
The cost of unclear APIs shows up in Slack, not logs
If your API requires a Slack thread to understand how to use it, it’s already broken.
Not because it doesn’t work, but because it creates friction at the exact point where developers expect speed: integration. Every “quick question” about payload shape, error meaning, or authentication nuance is a tax on both teams. Multiply that across services and you get slow onboarding, fragile integrations, and silent misuse.
The goal isn’t just correctness. It’s obviousness. A good API removes the need for interpretation.
Design for zero-questions, not flexibility
Most API design mistakes come from overvaluing flexibility and undervaluing predictability.
Example of a flexible but confusing endpoint:
POST /user
{
"type": "admin",
"data": {
"name": "Alice",
"permissions": ["read", "write"]
}
}
What’s unclear:
- Is
typerequired? - Does
permissionsapply only to admins? - What happens if
permissionsis omitted? - Are there other types?
Now compare with a more explicit design:
POST /admins
{
"name": "Alice",
"permissions": ["read", "write"]
}
You’ve traded flexibility for clarity. That’s usually the right trade.
If you must support multiple variants, make them structurally explicit:
POST /users/admin
POST /users/guest
Or at least enforce schemas with clear validation errors.
Error messages are part of the API contract
Most teams treat error responses as an afterthought. That’s where confusion explodes.
Bad error:
{
"error": "Invalid request"
}
Useful error:
{
"error": "INVALID_PERMISSION",
"message": "Permission 'delete' is not allowed for role 'editor'",
"field": "permissions"
}
This removes guesswork. The developer knows:
- what failed
- where it failed
- why it failed
If you’re using something like Spring Boot, define a consistent error structure via @ControllerAdvice:
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(InvalidPermissionException.class)
public ResponseEntity<ApiError> handleInvalidPermission(InvalidPermissionException ex) {
return ResponseEntity.badRequest().body(
new ApiError("INVALID_PERMISSION", ex.getMessage(), ex.getField())
);
}
}
Consistency matters more than completeness. A predictable error format beats a “smart” but inconsistent one.
Naming is a bigger deal than architecture
You can have perfect infrastructure—Kafka, Redis, rate limiting—and still ship a painful API if naming is sloppy.
Common mistakes:
- Overloaded terms (
data,info,payload) - Inconsistent pluralization (
/uservs/users) - Ambiguous verbs (
/process,/handle,/execute)
Prefer nouns and clear intent:
/orders/{id}/cancelinstead of/cancelOrder/invoices/{id}/paymentsinstead of/handlePayment
Developers should be able to guess endpoints without documentation—and be right most of the time.
Defaults should be safe and unsurprising
If an API behaves differently based on undocumented defaults, it’s going to cause production bugs.
Example:
GET /transactions
What’s unclear:
- Is it paginated?
- What’s the default page size?
- Is it sorted?
Make defaults explicit in both behavior and response:
{
"data": [...],
"pagination": {
"page": 1,
"pageSize": 25,
"total": 842
}
}
And enforce them consistently across endpoints. Don’t make /users behave differently from /transactions.
If you’re using Spring Boot, lock this down at the controller level:
@GetMapping("/transactions")
public Page<TransactionDto> getTransactions(
@PageableDefault(size = 25, sort = "createdAt", direction = Sort.Direction.DESC)
Pageable pageable
) {
return transactionService.findAll(pageable);
}
Now the behavior is predictable without reading docs.
Versioning is about stability, not control
Versioning is often used as a safety net for bad design. That’s backwards.
If you need frequent breaking changes, your API is too ambiguous or too tightly coupled to internal models.
Use versioning sparingly and deliberately:
- URI versioning:
/v1/orders - Header versioning:
Accept: application/vnd.api.v1+json
But more importantly, design for forward compatibility:
- Add fields, don’t repurpose them
- Avoid enum values that might change meaning
- Treat unknown fields as ignorable (per JSON RFC 8259)
A stable API reduces the need for versioning in the first place.
Documentation should confirm, not explain
If your documentation is doing heavy lifting, your API design is compensating for something.
Good docs:
- confirm assumptions
- show examples
- list edge cases
Bad docs:
- explain basic usage that should be obvious
- clarify inconsistent behavior
- include warnings like “be careful when…”
If you rely on OpenAPI (Swagger), generate it from code and enforce schema validation. But don’t expect it to fix poor design—it only reflects it.
Tradeoffs you can’t avoid
Designing for zero-questions comes with real costs:
- Less flexibility: You may need more endpoints or stricter schemas.
- Slower initial design: You’ll spend more time naming and structuring.
- Migration overhead: Fixing a bad API later is expensive.
But the upside compounds:
- Faster onboarding for new engineers
- Fewer integration bugs
- Less cross-team coordination
In most backend systems, that trade is worth it.
What to fix this week
Pick one API your team owns and look at it from a fresh developer’s perspective.
- Rename one ambiguous field or endpoint.
- Standardize one inconsistent error response.
- Make one default behavior explicit in the response.
If someone has to ask how to use it, treat that as a bug—not a documentation gap.