How do you design efficient APIs and data models for rich UIs?
Full-Stack Developer
answer
Great efficient API design starts with modeling tasks, not tables. Expose endpoints that map to UI intents, add query shaping (fields, include/expand, filters), and use pagination plus caching (ETag, stale-while-revalidate). For complex screens, add a Backend-for-Frontend (BFF) to aggregate data and prevent over-fetching. Use GraphQL (selection sets, fragments) or REST with sparse fieldsets and projections. Normalize writes, denormalize reads, and measure with p95 latency + payload budgets.
Long Answer
Delivering rich front-end experiences without wasting bandwidth or compute hinges on two pillars: efficient API design and pragmatic data modeling. The aim is simple: ship exactly what the UI needs—no more, no less—while keeping writes safe, reads fast, and evolution painless.
1) Start with jobs-to-be-done, not tables
Capture the screen contracts: what data a view needs above the fold, which filters/sorts exist, and how interactions mutate state. From these, derive API shapes that reflect tasks (e.g., GET /feed?after=…&fields=title,thumb,author) rather than raw tables. This avoids “CRUD leakage” that forces the client to call multiple endpoints and stitch results (classic over-fetching/under-fetching).
2) Query shaping on every read
Whether REST or GraphQL, give clients control over payload size:
- Sparse fieldsets / projections: ?fields=title,price,stock or Mongo/SQL projections.
- Include/expand: join selected relations only (?include=author,images).
- Pagination: prefer cursor-based pagination for stability under writes.
- Sorting/filtering: whitelist columns/ops; avoid ad-hoc server eval that explodes indexes.
In GraphQL, lean on selection sets, fragments, and @defer/@stream for gradual hydration of secondary data.
3) BFF to localize complexity
A Backend-for-Frontend sits between UI and microservices, aggregating and tailoring responses for each surface (web, mobile). The BFF encapsulates cross-cutting concerns—auth, caching, request coalescing—and prevents chatty clients. When moving from monolith to microservices, a BFF preserves efficient API design while the domain decomposes behind it.
4) Read-optimized models
Separate write models (normalized, transactional) from read models (denormalized, query-friendly) via CQRS/materialized views. Precompute listings and counters; keep them fresh with events or CDC. For heavy joins, generate view tables or search indexes (Elasticsearch/Meilisearch) that match top queries. This slashes under-fetching caused by repeated server joins.
5) Consistency & mutations
Design mutations around use cases (e.g., POST /orders/{id}/submit) rather than arbitrary PATCHes. Return the updated resource (or a delta) so the client can reconcile state without extra round-trips. Use idempotency keys to protect against duplicate posts. For GraphQL, apply input types, server-side validation, and publish subscriptions or SSE/WebSocket events to keep complex UIs reactive without polling storms.
6) Caching & freshness strategy
At the edge and client, use ETag/If-None-Match, Cache-Control, and stale-while-revalidate. Inside the BFF, add request coalescing (single-flight) to prevent thundering herds. For personalized data, cache fragments keyed by user/tenant and invalidation via events (e.g., “product-updated”). GraphQL can leverage persisted queries + CDN caching by hash.
7) Payload budgets & performance guards
Set budgets: <200 ms server time for above-the-fold, <100 KB JSON median, and p95 < 400 ms. Track over-fetch ratio (bytes returned / bytes rendered) and n+1 risk. In GraphQL, use cost analysis or depth limits; in REST, restrict include fan-out and page sizes. Add telemetry at the BFF to log query shapes and payload sizes per route.
8) Versioning & evolution
Prefer backward-compatible changes: add fields, don’t rename; use feature flags to stage larger shifts. In REST, version at the representation (Accept: application/vnd.app.v2+json) or at path when necessary. In GraphQL, deprecate fields with @deprecated and announce removal windows. Keep schema registries and contract tests to prevent drift.
9) Reliability across environments
Design timeouts, retries with idempotency, and circuit breakers in the BFF. Paginate expensive queries; protect search with request limits. For multi-tenant systems, scope indexes and keys by tenant to keep scans tight. Add SLOs and alerts on error rate, tail latency, and cache hit ratio.
10) Tooling & patterns checklist
- REST: JSON:API or similar conventions for fields/include/pagination.
- GraphQL: fragments, persisted queries, DataLoader for per-request batching.
- Storage: secondary indexes for hot filters; composite indexes for common sorts.
- Testing: contract tests (Pact), golden snapshots, and synthetic flows that measure bytes/latency.
The outcome: front-ends get precise, timely data; back-ends stay evolvable; and the network carries only what creates user value—hallmarks of efficient API design.
Table
Common Mistakes
Designing endpoints around tables, forcing clients to chain calls (under-fetching). Returning full objects by default with deep nested includes (over-fetching). Skipping cursor-based pagination, causing duplicates and gaps under writes. No BFF, so mobile/web re-implement aggregation and drift. Lack of projections/fields controls; payloads balloon. Zero caching headers; every view is a cache miss. GraphQL without cost/depth limits, enabling expensive queries. No read models—every list is a live multi-join. Finally, no telemetry: teams can’t see payload sizes, over-fetch ratio, or n+1 hotspots.
Sample Answers (Junior / Mid / Senior)
Junior:
“I add pagination and fields filters so the UI asks only for what it needs. If a screen needs multiple resources, I expose an aggregated endpoint to avoid chaining calls.”
Mid:
“I use a BFF that maps screens to routes, adds projections/include, and caches with ETag + SWR. Lists use cursor pagination; writes are idempotent and return updated resources so the UI doesn’t refetch. For GraphQL, I rely on selection sets and fragments.”
Senior:
“I split write vs read models (CQRS), precompute hot lists, and expose task-centric APIs. The BFF does request coalescing, enforces payload budgets, and logs over-fetch ratios. REST follows JSON:API conventions; GraphQL uses persisted queries and cost limits. We version compatibly, gate big changes with flags, and back everything with SLOs on p95 latency and cache hit rate.”
Evaluation Criteria
Look for: efficient API design tied to UI contracts; presence of BFF patterns; robust query shaping (fields/include/projections or GraphQL selection sets); correct pagination (cursor over offset); read-optimized models (CQRS/materialized views); caching strategy (ETag/SWR/CDN) and request coalescing; mutation design that returns updated state; governance (cost limits, payload budgets, telemetry); and evolution practices (backward compatibility, deprecations). Weak answers hand-wave “use GraphQL” without selection/cost controls or ignore BFF/read models.
Preparation Tips
Build a demo view (feed + sidebar + counters). First, wire naive CRUD and measure bytes/latency. Then: add fields/include, switch to cursor pagination, and introduce a BFF route that aggregates data. Create a read model (materialized view) for the feed, compare timings. Add ETag + SWR caching and single-flight in the BFF. In GraphQL, switch to persisted queries and measure cost reductions. Instrument telemetry: payload size, over-fetch ratio, p95. Prepare a 60–90s story showing before/after metrics and how efficient API design removed waste while improving UX.
Real-world Context
A SaaS dashboard loaded 7 endpoints per view; mobile suffered. Introducing a BFF with projections cut calls to one and trimmed payloads by 55%. Cursor pagination removed duplicates during live writes. Precomputing read models for “recent activity” turned a 1.8s join into a 120ms read. Adding ETag + SWR and request coalescing halved p95 latency at peak. Later, a GraphQL facade with persisted queries plus cost limits served the same screens with consistent bytes and fewer round-trips. The stack delivered richer UI while spending less CPU and bandwidth—proof of efficient API design in production.
Key Takeaways
- Model APIs around tasks, not tables.
- Use query shaping (fields/include/selection sets) and cursor pagination.
- Introduce a BFF to aggregate and cache per UI.
- Maintain read models (CQRS) to avoid heavy joins.
- Enforce caching, budgets, and evolution policies.
Practice Exercise
Scenario: You own a complex “Project Overview” screen (timeline, team, activity, KPIs). Today it hits 6 endpoints and still under-fetches details; payloads are large and p95 is 900 ms.
Tasks:
- Capture the screen contract: fields required above the fold vs. secondary panes.
- Add query shaping: REST fields/include (or GraphQL selection sets) to trim payloads to essentials.
- Replace offset with cursor pagination for activity and timeline.
- Create a BFF route /bff/projectOverview?id=… that aggregates project, team, KPIs, and first page of activity; add single-flight and ETag + SWR.
- Build a read model for KPIs/activity summaries (materialized view) refreshed by events; switch the BFF to read it.
- Design mutations as use-case endpoints (e.g., assignMember, updateMilestone) returning updated projections for instant UI reconciliation.
- Add telemetry: payload bytes, over-fetch ratio, p95 latency, and cache hit rate; set budgets and alerts.
- Optional: expose the same contract via GraphQL with persisted queries and cost limits.
Deliverable: a 60–90s narrative + before/after metrics demonstrating reduced round-trips, smaller payloads, and faster p95 without losing correctness.

