Your API Is a Product. The Developer Is Your Customer.
by Eric Hanson, Backend Developer at Clean Systems Consulting
The cost of treating APIs as “just an interface”
Most teams don’t set out to build bad APIs. They just treat them as implementation details.
You see it in subtle ways:
- Endpoints mirror database tables
- Breaking changes sneak into “minor” releases
- Error responses are inconsistent or undocumented
- Pagination, filtering, and auth behave differently across services
Then six months later, onboarding a new consumer takes weeks, not days. Internal teams start wrapping your API with their own abstractions just to survive it. External users open support tickets for things that should have been obvious.
At that point, the API is no longer a technical concern. It’s a product failure.
If other engineers depend on your API to do their job, they are your customers. Whether you acknowledge that or not just determines how painful the experience will be.
Design for usage, not for implementation
A common anti-pattern is designing APIs directly from the data model:
GET /users
GET /orders
GET /order_items
This is convenient for the backend, but it forces the client to orchestrate everything.
Instead, design around use cases:
GET /customers/{id}/orders?status=active
That shift matters. You’re reducing round trips, simplifying client logic, and encoding intent directly into the API.
If you’re using REST, this often means aggregating resources at the boundary. If you’re using GraphQL, it means being disciplined about your schema design rather than exposing raw entity graphs.
A useful heuristic: if the client has to understand your internal data relationships to use your API efficiently, you’ve leaked implementation details.
Versioning is not optional, but it’s easy to get wrong
You don’t get to skip versioning. The only choice is whether you do it deliberately or accidentally.
The simplest reliable approach is URI versioning:
GET /v1/customers/{id}
It’s not elegant, but it’s explicit and operationally simple. Header-based versioning (e.g., Accept: application/vnd.myapi.v2+json) looks cleaner but tends to get lost in proxies, logs, and debugging workflows.
More important than how you version is how you evolve:
- Additive changes (new fields, optional params) should not require a version bump
- Breaking changes must go into a new version, period
- Old versions need a deprecation window with real timelines
A mistake I’ve seen repeatedly: teams introduce /v2 but keep changing /v1 anyway. That defeats the entire purpose.
If you can’t commit to supporting at least one previous version for a defined period, you’re pushing operational complexity onto every consumer.
Error handling is part of your API contract
Most APIs treat errors as an afterthought. That shows up immediately in client code.
Bad:
{
"error": "Something went wrong"
}
Useful:
{
"error": {
"code": "ORDER_NOT_FOUND",
"message": "Order 1234 does not exist",
"details": {
"order_id": "1234"
}
}
}
This isn’t about verbosity. It’s about making errors machine-readable.
If your consumers can’t reliably distinguish between a validation error and a transient server issue, they will either:
- Retry everything (and amplify outages), or
- Retry nothing (and degrade UX)
Pick a consistent structure and stick to it. If you’re in the HTTP world, align with RFC 7807 (Problem Details for HTTP APIs). It’s widely supported and solves this problem cleanly.
Performance is a feature, but measure it properly
You don’t need to prematurely optimize, but you do need to understand the cost of your API shape.
Example: an endpoint that returns a customer and their last 50 orders.
On a dataset of:
- 1M customers
- 50M orders
- PostgreSQL on a 4 vCPU / 16GB instance
A naive implementation with N+1 queries might look fine in dev but collapse under concurrency.
A more realistic approach:
SELECT c.*, o.*
FROM customers c
LEFT JOIN LATERAL (
SELECT *
FROM orders o
WHERE o.customer_id = c.id
ORDER BY o.created_at DESC
LIMIT 50
) o ON true
WHERE c.id = $1;
Then shape the response in the application layer.
Test it under load:
- 200 concurrent requests
- realistic payload sizes (~100KB responses)
If p95 latency jumps from 80ms to 600ms when concurrency increases, your API design is the problem, not just your query.
The point: performance characteristics are part of the API contract. Clients build expectations around them.
Consistency beats cleverness
If one endpoint uses cursor-based pagination:
GET /orders?cursor=abc123&limit=50
and another uses offset-based:
GET /customers?page=2&page_size=50
you’ve already made the API harder to use than it needs to be.
Pick patterns and apply them everywhere:
- Pagination (cursor vs offset)
- Filtering (
?status=active&created_after=...) - Sorting (
?sort=-created_at) - Field selection (
?fields=id,name,email)
This is where API guidelines pay off. Not theoretical ones — enforced ones. Linting OpenAPI specs with tools like Spectral helps catch drift before it ships.
Documentation is not a separate deliverable
If your API requires a long-form guide to explain basic usage, the design is doing too much work.
Good APIs are self-explanatory at the edges:
- predictable naming
- consistent patterns
- sensible defaults
Documentation should focus on:
- edge cases
- workflows (not endpoints)
- constraints and guarantees
Auto-generated docs from OpenAPI are useful, but they’re not enough. They describe what exists, not how to use it effectively.
The tradeoffs you can’t ignore
Treating your API as a product has real costs:
- Slower initial development: You’ll spend more time on design reviews and consistency
- Backward compatibility burden: Supporting multiple versions increases maintenance overhead
- Internal friction: Teams will push back when they can’t “just change the API”
But the alternative is worse:
- fragmented clients
- duplicated logic across services
- brittle integrations that slow down the entire organization
You’re choosing where to pay the complexity tax.
What to do differently this week
Pick one API your team owns and do a quick audit:
- Does it expose internal data structures directly?
- Are errors consistent and machine-readable?
- Can a new developer use it without tribal knowledge?
- Are breaking changes clearly versioned?
Fix one of those, not all of them.
The shift happens incrementally. But once you start treating the API as a product, your decisions get sharper — and your consumers notice quickly.