count, sum, average, percentile. React features
used: <Suspense> boundaries, async server components.
What You’ll Build
An admin dashboard with four tiles:- Orders today
- Revenue today
- Average order value
- p95 fulfillment time
The Order Model
One Component Per Metric
Each tile owns its own query. Keeping them separate is the whole point — it’s what lets Suspense stream them in independently:todayOrders()is a factory. It returns a freshQueryBuildereach time. You can’t reuse a single builder acrosscount()andsum()— every terminal call needs its own builder.- Filter before percentile. Including rows where
fulfilledAtisnullwould skew the distribution. The chainedwhere('fulfilledAt', '!=', null)fixes that. - Aggregates return
0on empty results. No null-handling needed even on a brand-new day.
The Dashboard Page
Wrap each tile in its own Suspense boundary. The page itself doesn’tawait
anything — Next.js streams the fallback skeletons first, then swaps in each
tile as its query resolves:
export const dynamic = 'force-dynamic' is the right choice here — a
dashboard should never be cached.
Why Streaming Beats Promise.all
You could write this with a single page-level await Promise.all([...]) and
hand every metric to one tile component. It would render correctly, but the
user wouldn’t see anything until the slowest query finished. With one
Suspense boundary per tile, fast metrics appear right away and slow ones
catch up. Same total work, much better perceived performance.
Pattern Notes
- One boundary per tile. Boundaries are what unblocks streaming — without
them, an
awaitfurther down still blocks the page shell. - Don’t pre-await in the parent. Once the parent component is async and blocks on data, you’ve lost streaming for its whole subtree.
- Watch out for caching. Default fetch caching doesn’t apply to direct
database calls, but Next.js’s
unstable_cachedoes. Wrap each metric inunstable_cachewith a short TTL if you want to share results across requests.
What’s Next
- Querying in Server Components — the foundational pattern these tiles build on.
- Filter and Paginate — pair the dashboard with a drill-down list view.
- Aggregation Dashboard — the Express version of these helpers in one big JSON response.