Skip to main content

Building a Returns Order Lifecycle Dashboard with Node.js and GraphQL

00:02:58:50

The tab problem

When I joined the returns team, tracking a single return meant opening four systems. One held the original order. Another had the return authorization. Shipping status lived somewhere else. Refund state was in a fourth place, and it lagged the others by a few minutes, which meant an associate could truthfully tell a customer two contradictory things in the same call.

Nobody had designed this. It accreted. Each system was reasonable on its own and the seams between them were where the work happened — all of it manual, all of it in someone's head.

What we needed was one view of a return's whole life.

Why GraphQL

Three reasons, in rough order of how much they mattered.

The first was aggregation. Our data lived behind several REST services, and a return's story needed pieces from all of them. GraphQL let us present that as one query instead of making the frontend orchestrate four calls and reconcile the results.

The second was iteration speed. Once the schema covered a field, the frontend could start using it without waiting on a backend release. On a team where the UI questions were still being figured out, that removed a lot of back-and-forth.

The third was batching. DataLoader collapsed the repeated per-record lookups that the naive version generated, and cut load on the downstream services by about 70%. That number is really a measure of how wasteful the first implementation was — but it's also the reason the downstream teams stopped asking us to slow down.

Node with Apollo Server on the backend, React with Apollo Client on the front. REST APIs wrapped as GraphQL data sources. Subscriptions over WebSockets for live updates. Redis for response caching.

The timeline

The timeline view was the hardest thing to build and the thing people ended up opening first every morning. It shows a return's complete journey in one column — every state change, in order, with timestamps.

Getting it right was mostly a data problem rather than a UI one. The source systems disagreed about ordering when events landed within the same second, and they used different vocabularies for what turned out to be the same state. Reconciling that took longer than building the component that displayed it.

Live updates were the part that changed how the thing felt. When a return's status changes, every associate looking at it sees the change immediately. Before that, two people could be working the same return from different information, and the first sign of trouble was a customer being told something wrong.

What changed

Average handle time per return dropped around 60%. Refunds moved through about 30% faster. Roughly 200 associates across customer service picked it up. Everyone could see the same state at the same time, and because tracking was finally standardized, the reporting built on top of it meant something for the first time.

The parts that made building it pleasant

Strong typing caught a genuine amount of nonsense before it ran. GraphQL Playground meant I could answer "what does this return?" myself instead of asking. The schema generated its own documentation, which mattered more than expected when new people joined. And codegen from schema to TypeScript types removed the class of bug where the frontend and backend quietly disagree about a field's shape.

The lasting lesson is narrower than "GraphQL is good." It's that the best internal tools are the ones people stop noticing. Nobody has ever complimented this dashboard. They just stopped keeping four tabs open, which is the same thing.