Fundamentalsconsumerstasksgranularityclients

Consumer-First Design

APIs exist for consumer tasks, not for the provider's data model. A mobile dashboard, a partner's invoice integration and an internal inventory call want different granularity, different fields and different guarantees from the same domain.

Follow the failure

Frame the contract

API design starts with a consumer, a design question and a guarantee — never with a URL.

Design question
Which consumer tasks must this API make easy, and what does each consumer's environment demand from the contract?
Consumers
Three archetypes pulling in different directions: a mobile client on a flaky radio that must render a screen in one round trip; an external developer scripting against documentation alone; an internal service that calls ten thousand times a minute and cares about tail latency.
The promise
A consumer-first contract lets each consumer finish its task in the small number of calls its environment can afford, without over-serving one consumer at every other's expense.
RequirementConsumersResource ModelStyleContractValidationAuthorizationErrorsIdempotencyPaginationVersioningObservabilityEvolutionTrade-offs

Tasks, not tables

The unit of API design is the consumer task: "load the dashboard", "create an invoice and know it was created exactly once", "reserve inventory for this order". Consumers do not want your entities; they want their task finished. An API that mirrors the schema forces every consumer to reassemble tasks out of entity fragments — the Over-Fetching and Under-Fetching and chatty-API problems are both downstream of this one decision.

Writing the task list first also exposes the real variance between consumers. "Load the dashboard" needs one aggregated read, tolerates thirty seconds of staleness and must survive a lossy network. "Reserve inventory" needs strong consistency, an idempotency story and a two-digit-millisecond budget. No single endpoint style serves both well — and that is information, not a problem.

The same domain, three consumers, three different contracts
ConsumerTaskWhat the environment demands
Mobile appLoad dashboard, update profile, upload imageFew round trips, small payloads, tolerance for stale data, resumable uploads. Old versions live for months.
External developerCreate invoice, query status, receive webhookStable errors, Idempotency Keys: The Mechanism, documentation that is the whole truth, a compatibility horizon measured in years.
Internal serviceReserve inventory, fetch pricing, emit payment resultTyped contracts, low latency, high volume, fast evolution with tracked consumers. See gRPC: Schema, Codegen and Streams.

Consumer context changes the right answer

Granularity is the clearest example. For the mobile client, a coarse GET /dashboard that aggregates five entities is a gift: one round trip on a 300ms-RTT radio instead of five. For the internal service, the same aggregation is waste — it needs exactly one price, ten thousand times a minute, and every extra joined entity is latency and load. Serve both from one endpoint and you will serve both badly.

The same divergence appears in every later lesson: pagination style depends on whether the consumer resumes or jumps; error detail depends on whether a human or a program reads it; versioning policy depends on whether you can force consumers to upgrade. Consumer context is not a nice-to-have persona exercise — it is the input the rest of the design consumes.

When consumer needs genuinely conflict, the answer is usually not a compromise endpoint but a deliberate split: a Backend for Frontend for the experience-shaped reads, and narrow capability APIs for the service-to-service calls.

screen-shapedstable, documentedMobile appWeb appPartner integrationExperience API (BFF)Public APICapability APIs
UserLLMAgentToolDataDecisionHumanGuardrail

Write the client code first

The cheapest consumer-first technique costs one page: before designing the API, write the code a consumer would *like* to write for each task — the ideal SDK call, the ideal fetch sequence for the screen. If the imagined code is awkward, the API it implies will be worse, because real consumers also handle errors, retries and empty states.

This is the API-design version of test-first: the "test" is the consumer's program. It catches chattiness ("why does rendering one screen take eight awaits?"), missing aggregates, and guarantee gaps ("this retry loop is unsafe — nothing here is idempotent") while they are still free to fix.

Design artifact: the code the mobile team wishes it could write
1const dash = await api.dashboard.load() // one round trip, staleness OK
2render(dash)
3
4const upload = await api.images.createUpload({ bytes: file.size })
5await putWithResume(upload.url, file) // direct to storage, resumable
6await api.profile.update({ avatar: upload.id }) // safe to retry: idempotent

Key points

  • Design for consumer tasks, not entities; consumers want their task finished in the calls their environment can afford.
  • Different consumers legitimately need different granularity, freshness and guarantees from the same domain.
  • Conflicting consumer needs are resolved by splitting surfaces (BFF + capability APIs), not by one compromise endpoint.
  • Writing the ideal client code first exposes chattiness and guarantee gaps before they ship.
  • Consumer context is the input to every later decision: pagination style, error detail, versioning policy.

Follow the failure

How the contract fails or gets misused, hop by hop — and what it costs when it completes.

  1. 1
    Team → API: exposes the entity model directly because it is what the ORM already has.
  2. 2
    Mobile client → API: needs eight calls to render one screen; ships them because there is no alternative.
  3. 3
    Users → app: the screen takes seconds on mobile networks; the app team adds a caching layer with its own bugs.
  4. 4
    Internal service → API: fetches a heavyweight aggregate to read one field; p99 latency and payload cost climb.
  5. 5
    Team → API v2: a "performance rewrite" is scheduled — actually a consumer-first redesign, done late and under pressure.
What breaks
  • Mobile experience: latency multiplies by round trips; every screen pays the chattiness tax.
  • Backend load: N calls per screen instead of one; the provider scales infrastructure to compensate for contract shape.
  • Team velocity: every client builds and maintains its own aggregation/caching layer to survive the entity-shaped API.

Design, observe, evolve

A contract decision is incomplete until you know how you would notice it failing and how it changes later.

Design the contract
  • • Enumerate consumers and their tasks first; make "which consumer, which task?" a required field on every endpoint proposal.
  • • Shape reads around tasks (screens, workflows) and writes around domain operations, not around tables.
  • • Split surfaces when consumer needs conflict: experience APIs for clients, capability APIs for services (see [[backend-for-frontend]]).
  • • Prototype the ideal consumer code for each task and review the API against it.
Observe in production
  • • Count round trips per consumer task in real traffic: a screen that needs more than two sequential calls is a contract smell.
  • • Watch which fields consumers actually read (SDK telemetry, GraphQL field stats, sampled logging): entity-shaped responses show single-digit field usage.
  • • Track per-consumer latency budgets separately; an internal p99 regression and a mobile round-trip regression have different fixes.
Evolve without breaking
  • • Task-shaped endpoints evolve with the product: when the dashboard changes, its endpoint changes, without disturbing the capability APIs underneath.
  • • New consumer types get new surfaces rather than mutations of existing ones — the partner API stays stable while the BFF iterates weekly.
What it costs
  • • More surface to own: task-shaped endpoints multiply with consumer types, and each needs an owner and tests.
  • • Aggregated reads couple the aggregator to several sources; its latency is their slowest member (see [[api-composition]]).
  • • Task-shaped APIs are opinionated: a consumer with a genuinely new task needs API work, where a generic entity API would have (badly) let them self-serve.

Misconceptions

Claim
“A generic entity API serves every consumer — that is what makes it general.”
Reality
It serves every consumer equally badly: the mobile client over-fetches and multiplies round trips, the service over-pays for aggregates. Generality of *data access* is not generality of *task support*.
Claim
“GraphQL makes consumer-first design automatic.”
Reality
GraphQL moves field selection to the client, which helps over-fetching — but task shape, guarantees, idempotency and rate behavior are still contract decisions the schema designer must make. See GraphQL: Client-Shaped Queries Over One Schema and What GraphQL Costs.

Apply it