/products?category=lamps&page=2 — pages
become shareable, the back button works, and the server has everything it
needs on the first request. This recipe shows how to build that page in the
App Router with Esix doing the heavy lifting.
Esix features used: paginate, where, whereIn, orderBy. Next.js
features used: searchParams prop on async server components.
What You’ll Build
A page at/products that reads filters from the URL, runs a single Esix
query, and renders the results plus next/prev links:
The Product Model
Parsing Search Params
searchParams arrives as Record<string, string | string[] | undefined> —
narrow it to a typed shape before touching Esix. A zod schema gives you the
parsing, validation, and the TypeScript type from a single definition:
perPage. z.coerce.number() is the key
piece: every search param arrives as a string, and the schema converts and
validates in one step.
The Page Component
searchParams is passed directly to the page as a prop. Build the query
conditionally and call paginate once:
paginate returns everything you need to render the navigation — page,
lastPage, total, perPage, and the page of data itself.
Building Page Links
Pager links should preserve every other filter — drop the user back where they were, just on a different page. A tiny helper rebuilds the query string:<Link> does client-side navigation when possible, but on the server side the
page re-runs with the new searchParams, the Esix query re-runs, and the new
HTML streams down. The catalog stays fully indexable and shareable.
Filter UI
A plain HTML form is enough — point it at the current page withmethod="get"
and the browser does the URL building for you:
useState hook — submitting reloads the page with the
new search params, which is exactly what we want.
Pattern Notes
- URL is the state machine. Every filter that influences what the page
shows belongs in
searchParams. Server components re-render on URL change for free. - Cap
perPage.z.coerce.number().max(100)does the work — a client setting?perPage=1000000would otherwise be the path of least resistance to an OOM. paginateover hand-rolledskip/limit. You gettotalandlastPageback, which is exactly what the Pager component needs — and Esix validates the inputs for you.
What’s Next
- Querying in Server Components — the foundational patterns this page builds on.
- Streaming Aggregations — pair this list view with summary metrics that stream in.
- Pagination — reference for the
paginateresponse.