Sorting
Every customer-facing list on the storefront is admin-curated by default.
Operators drag rows in the admin panel, the panel sends a single reorder
call, and the public client API picks up the new order on the very next
request — no cache flush, no deploy.
This guide documents:
- The
sort_ordercolumn convention used across the schema. - The admin reorder API for every curated resource.
- The client-side sort knobs (
?sort=,?variant_sort=) that override the default ordering when the storefront needs them.
The sort_order column
Every curated table carries the same shape:
sort_order INTEGER DEFAULT 0CREATE INDEX idx_<table>_sort_order ON <table> (sort_order);Tables with sort_order today:
| Table | Surfaced on client API |
|---|---|
brands | GET /catalog/brands |
categories | GET /catalog/categories |
tags | GET /catalog/tags |
product_types | GET /products/types |
products | GET /products (list) |
product_variants | variants[] inside GET /products / GET /products/:id |
payment_methods | GET /payment-methods |
faqs | GET /faq |
homepage_banners, homepage_sections, homepage_section_items | GET /homepage |
Default ordering on every list endpoint is:
ORDER BY sort_order ASC, id ASCid is the deterministic tiebreaker so equal sort_order values still
produce a stable page — important for pagination.
Admin reorder API
A single canonical contract is used across every curated resource. The body
is a ReorderDto:
{ "items": [ { "id": 7, "sort_order": 0 }, { "id": 2, "sort_order": 1 }, { "id": 11, "sort_order": 2 } ]}| Rule | Behaviour |
|---|---|
items.length | 1 – 500 per request. 0 returns 400. >500 returns 400. |
Duplicate ids | Rejected with 400. |
Unknown ids | Rejected with 404 (<entity> id X not found). |
| Atomicity | Every update runs inside one Prisma $transaction. Either all rows are updated or none. |
| Response | { success: true, message: "<Resource> reordered", data: { updated: <count> } } |
Endpoint matrix
| Method | Path | Resource |
|---|---|---|
PATCH | /admin/brands/reorder | Brands |
PATCH | /admin/categories/reorder | Categories |
PATCH | /admin/tags/reorder | Tags |
PATCH | /admin/product-types/reorder | Product types |
PATCH | /admin/payment-methods/reorder | Payment methods |
PATCH | /admin/products/reorder | Products (global ordering across the catalogue) |
PATCH | /admin/products/:productId/variants/reorder | Variants inside one product |
POST | /admin/homepage/banners/reorder | Homepage banners |
POST | /admin/homepage/sections/reorder | Homepage sections |
POST | /admin/homepage/sections/:sectionId/items/reorder | Items inside one homepage section |
Example
PATCH /admin/products/42/variants/reorderAuthorization: Bearer $ADMIN_TOKENContent-Type: application/json
{ "items": [ { "id": 311, "sort_order": 0 }, { "id": 309, "sort_order": 1 }, { "id": 312, "sort_order": 2 } ]}Response:
{ "success": true, "message": "Variants reordered", "data": { "updated": 3 }}The change is visible immediately on the next GET /products/42 call.
Client-side overrides
The default sort_order ASC, id ASC ordering is always available, but the
client may request a different order via query params on the list endpoints.
GET /products — ?sort=
?sort=sort_order.asc # default?sort=sort_order.desc?sort=name.asc?sort=name.desc?sort=price.asc # cheapest first (computed from cheapest variant)?sort=price.desc?sort=newest # created_at DESCUnsupported values fall back to the admin-curated default.
GET /products & GET /products/:id — ?variant_sort=
The variant array embedded in each product is independently sortable:
?variant_sort=price # default — cheapest first, then admin sort_order, then id?variant_sort=sort # admin-curated (matches /admin/products/:id/variants/reorder)?variant_sort=name # alphabetical by variant name| Mode | Resulting ORDER BY |
|---|---|
price | price ASC, sort_order ASC, id ASC |
sort | sort_order ASC, id ASC |
name | name ASC, id ASC |
Catalog endpoints — ?sort=
GET /catalog/categories, /catalog/brands, /catalog/tags all accept:
?sort=sort_order.asc # default?sort=sort_order.desc?sort=name.asc?sort=name.desc?sort=id.asc?sort=id.descPredictable pagination
Because every endpoint uses id as the secondary key, pagination is
stable even when many rows share the same sort_order (for example,
right after a fresh import where everything is 0). Operators can safely
drag-reorder a single row without worrying about the rest of the page
shifting under them.
Related guides
- Admin product management — where to set
sort_orderinline when creating a product/variant. - Orders — order ordering is unrelated; orders sort by
created_at DESCby default.