Skip to content
4 min read

Partial API Responses for Better UX

Neon Skies and Chrome Dreams

I am writing as a 5+ YOE engineer.

When “Complete Data” Became a UX Bug

In one production flow, our product page API returned a fully assembled payload in around five seconds. Technically correct, but behaviorally wrong. Session analytics showed a large drop-off after two to three seconds, so users were abandoning before they saw any meaningful content.

After profiling the request path, we found an obvious asymmetry: product identity fields were fast, while pricing depended on a slower downstream pipeline. Because the endpoint was all-or-nothing, the slowest field delayed the entire response. We were optimizing payload purity while hurting user trust and task completion.

The Contract We Shipped Instead

We changed the contract from “everything now” to “everything ready now, pending explicitly later.” The primary endpoint returns core data immediately plus a tracking token and completion flag for slow fields.

{
  "id": "123",
  "track_id": "track-999",
  "name": "Running Shoes",
  "description": "Lightweight running shoes for daily training.",
  "seller": "Sport Store",
  "address": "Jl. Example 123",
  "price": null,
  "priceComplete": false
}

This made the response semantically honest: the product exists, most fields are ready, and pricing is still in progress.

Completion Endpoint and UI Behavior

Instead of re-requesting /product, the client calls a dedicated completion endpoint with track_id.

POST /getPrice
Content-Type: application/json
{
  "track_id": "track-999",
  "productId": "123"
}

If price computation is still running, the endpoint returns priceComplete: false; when done, it returns final price and priceComplete: true. The UI renders identity fields immediately, keeps a scoped loading state only for price, and stops retrying once the completion flag is terminal.

Terminal Null Is Not the Same as Loading

A critical edge case is terminal failure where computation ends without a usable price.

{
  "track_id": "track-999",
  "price": null,
  "priceComplete": true
}

This state must not be treated as “still loading.” In our implementation, we stopped retries and applied a deterministic fallback (for example, hide item in price-sensitive listings). Making this explicit prevented infinite loading loops and inconsistent UI behavior across devices.

Flow Diagram

Partial Response Flow Diagram

Flow diagram showing the partial response pattern

Risks and Tradeoffs

Partial responses improve perceived performance, but they increase contract complexity because backend and client must agree on pending, complete, and terminal-failure states with strict semantics. They also add operational concerns such as retry policy tuning, idempotency expectations, and stale-state handling when users navigate quickly between screens or resume apps after backgrounding.

Lessons Learned

The biggest gain came from modeling state transitions explicitly rather than chasing smaller infrastructure optimizations first. Once we defined response states as part of the API contract, UI behavior became predictable, observability became easier, and product discussions shifted from “why is loading slow” to “which state is user seeing and why.”

Field Tips

Start with one slow field that is business-important but not required for first paint, then split that field into a completion workflow with a clear tracking token and terminal state definitions. Instrument each state transition on both backend and client, and review failure paths early, because most UX regressions in this pattern come from ambiguous terminal behavior, not from the happy path.

Authoritative References

Conclusion

Partial responses are not a framework trick; they are a contract design choice. Returning ready data early while modeling pending work explicitly gave us faster perceived UX without pretending slow dependencies did not exist. The payload became less “perfect,” but the user journey became measurably better and more reliable in production.