Skip to main content
Most apps eventually need a metrics endpoint — total orders, revenue, average order value, response-time percentiles, top categories. This recipe walks through building one in Express using Esix’s aggregation helpers, plus a single raw aggregate pipeline for the breakdown that doesn’t fit the helpers. Esix features used: count, sum, average, percentile, chained where(...).sum(...), and the raw aggregate pipeline.

What You’ll Build

Response:

The Order Model

placedAt and fulfilledAt are unix milliseconds — easy to filter against and easy to serialise from a JSON request.

The Metrics Endpoint

Define the query-string contract with zod, then compute all five metrics in parallel with Promise.all. Each helper is its own MongoDB round-trip, so doing them concurrently keeps the endpoint snappy:
A few things to notice:
  • inRange() is a factory, not a stored query. Each call returns a fresh QueryBuilder. Reusing a single instance across count() and sum() would not work — once a terminal method runs the builder is consumed.
  • Aggregates return 0 on empty results. No need for null checks on the numeric metrics.
  • Filtering chains naturally into aggregates. The p95 line filters out unfulfilled orders before computing the percentile.

Grouped Breakdowns with aggregate

count / sum / average are perfect for single numbers, but the top categories list needs a $group stage. Drop down to MongoDB’s pipeline:
aggregate is the escape hatch when the chained helpers don’t cover what you need. Keep the pipeline beside the model rather than inside the route handler so the handler keeps reading like a summary, not a query plan.

Trying It Out

Pattern Notes

  • Five round trips beats one cluttered pipeline. When each metric is its own simple call, the code reads top-to-bottom and individual metrics are easy to test in isolation. Reach for a single big $facet only when latency numbers tell you to.
  • Percentiles need a clean dataset. Filter out rows that don’t have the measured field (fulfilledAt !== null) before calling percentile, otherwise you’ll pollute the distribution with zeroes.
  • Type the pipeline result. Order.aggregate<CategoryRow>(...) keeps the raw MongoDB shape — including _id — under your control.

What’s Next