Dynamic Product Listing Module — Storefront
Public storefront surface for reading dynamic product listing groups by slug and resolving dynamic product listings (saved filter specs) into live product results from the search engine.
HTTP surface for reading dynamic product listing groups by slug and resolving dynamic product listings (saved filter specs) into live product results. Groups and listings are created and managed via the admin surface; the storefront reads them here.
Source:
api-modules/dynamic-product-listing/src/controllers/store-dynamic-product-listing-group.controller.ts,api-modules/dynamic-product-listing/src/controllers/store-dynamic-product-listing.controller.ts.Admin CRUD for groups and listings is in
docs/separated/admin/dynamic-product-listing.md. The storefront resolves a listing's savedfiltersinto matching products at request time, with optional per-request overrides.
Conventions
Authentication
| Endpoint | Auth |
|---|---|
GET /store/dynamic-product-listing-groups | none (public) |
GET /store/dynamic-product-listing-groups/slug/:slug | none (public) |
GET /store/dynamic-product-listings/slug/:slug | none (public) |
GET /store/dynamic-product-listings/slug/:slug/products | none (public) |
All storefront endpoints are public — no session required.
Response envelope (standard endpoints)
Standard group/listing reads are wrapped by ResponseInterceptor:
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": { /* pagination: total, limit, offset, hasMore */ }
}Response envelope (/products endpoint — legacy page-based search)
The /products endpoint passes the product-search response verbatim — it uses the legacy page-based search envelope and is not wrapped by ResponseInterceptor:
{
"data": {
"products": [ /* product objects */ ],
"brands": [ /* facet */ ],
"ingredients": [ /* facet */ ],
"attributes": { /* facet map */ }
},
"metadata": {
"total": 120,
"items": 20,
"perPage": 20,
"currentPage": 1,
"lastPage": 6
}
}This is the legacy page-based pagination shape (
perPage / currentPage / lastPage), not the standard offset-based{ total, limit, offset, hasMore }used elsewhere. It is intentional — the/productsendpoint is a verbatim passthrough to the product-search engine.
Error envelope
statusCode | errorCode examples |
|---|---|
| 404 | NOT_FOUND |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Domain types
DynamicProductListingGroupResponse
type DynamicProductListingGroupResponse = {
id: string;
title: string;
slug: string; // lowercase alnum + hyphens
metadata: Record<string, unknown> | null;
createdAt: string; // ISO
updatedAt: string;
};
type DynamicProductListingGroupWithListingsResponse = DynamicProductListingGroupResponse & {
listings: DynamicProductListingResponse[]; // nested listings with filters
};DynamicProductListingResponse
type DynamicProductListingResponse = {
id: string;
title: string;
slug: string;
groupId: string | null;
subtitle: string | null;
description: string | null;
image: string | null;
order: number;
filters: ListingFilters | null; // saved filter spec
metadata: Record<string, unknown> | null;
createdAt: string;
updatedAt: string;
};Endpoints
GET /store/dynamic-product-listing-groups — List groups
Returns all groups (paginated). Standard offset pagination.
Query params
| Name | Type | Notes |
|---|---|---|
limit | int? | Default 20 |
offset | int? | Default 0 |
Response 200 — paginated envelope of DynamicProductListingGroupResponse[].
GET /store/dynamic-product-listing-groups/slug/:slug — Get a group with its listings
Returns the group plus its nested listings array (each listing includes its filters field). Used to build a collections page or a carousel of curated filter-driven product shelves.
Path params
| Name | Notes |
|---|---|
slug | Group slug (lowercase alnum + hyphens) |
Response 200 — DynamicProductListingGroupWithListingsResponse.
{
"data": {
"id": "01J9aaaa...",
"title": "Summer Collection",
"slug": "summer-collection",
"metadata": null,
"createdAt": "2026-04-01T08:00:00.000Z",
"updatedAt": "2026-05-01T08:00:00.000Z",
"listings": [
{
"id": "01J9bbbb...",
"title": "Budget Supplements",
"slug": "budget-supplements",
"groupId": "01J9aaaa...",
"subtitle": "Great value picks",
"description": null,
"image": "listings/budget-supplements.jpg",
"order": 0,
"filters": {
"categories": ["supplements"],
"maxPrice": 50000,
"inStock": true,
"sortBy": "price-asc"
},
"metadata": null,
"createdAt": "2026-04-15T08:00:00.000Z",
"updatedAt": "2026-04-15T08:00:00.000Z"
}
]
},
"message": "Success",
"statusCode": 200
}Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | No group matches the slug |
GET /store/dynamic-product-listings/slug/:slug — Get one listing
Returns a single listing's metadata including its saved filters spec, without resolving products. Use this to display the listing's title / image / description and then call /products separately.
Path params
| Name | Notes |
|---|---|
slug | Listing slug |
Response 200 — DynamicProductListingResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | No listing matches the slug |
GET /store/dynamic-product-listings/slug/:slug/products — Resolve listing into products
Fetches the listing's saved filters spec and passes it to the product-search engine, returning matching products and facets. Runtime query params can override the saved spec on a per-field basis — useful for client-side sort/filter controls on a listing page.
pageandlimitalways come from the request (never from the saved spec).
Envelope: the response is passed verbatim from the product-search engine — the legacy page-based
metadata: { total, items, perPage, currentPage, lastPage }shape. This is intentional and differs from the standard offset-based pagination used elsewhere.
Path params
| Name | Notes |
|---|---|
slug | Listing slug |
Query params — all optional; any supplied field overrides the corresponding field in the listing's saved filters:
| Name | Type | Notes |
|---|---|---|
q | string? | Full-text search term |
vendors | string? | CSV of vendor ids |
brands | string? | CSV, e.g. acme,goodlife |
categories | string? | CSV |
tags | string? | CSV |
ingredients | string? | CSV |
skus | string? | CSV |
attributes | string? | JSON string, e.g. {"flavour":["mango"]} |
minPrice | int? | Integer subunits |
maxPrice | int? | Integer subunits |
inStock | "true" | "false"? | String boolean |
hasActiveSpecial | "true" | "false"? | String boolean |
withReviews | "true" | "false"? | Embed each product's latest approved reviews (reviews[], up to 10). Also settable as a saved option on the listing's filters; the runtime param overrides it. |
sortBy | string? | relevance | price-asc | price-desc | new | best-selling | inventory-high | inventory-low | rating-desc | rating-asc |
page | int? | Default 1; always from request |
limit | int? | Default 20; always from request |
Response 200 — verbatim product-search engine envelope:
{
"data": {
"products": [
{
"id": "01J9cccc...",
"title": "Whey Protein 1kg",
"slug": "whey-protein-1kg",
"price": 189900,
"rating": { "average": 4.5, "count": 12 }
// ... standard product fields
}
],
"brands": [ { "id": "acme", "label": "Acme Nutrition", "count": 12 } ],
"ingredients": [],
"attributes": { "flavour": [ { "value": "mango", "count": 4 } ] }
},
"metadata": {
"total": 120,
"items": 20,
"perPage": 20,
"currentPage": 1,
"lastPage": 6
}
}Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | No listing matches the slug |
Related modules
storage— resolvesimagekeys to CDN URLs on the client.search— the underlying product-search engine; the/productsenvelope is its native format.admin/dynamic-product-listing— admin CRUD for groups and listings. Seeadmin/dynamic-product-listing.md.
Dynamic Link Module — Storefront
HTTP surface for reading dynamic link groups by slug. Dynamic link groups are CMS-style ordered collections of {image, text, url} cards — used for the storefront home grid, promo…
Frequently Bought Together Module — Storefront
Public, anonymous storefront surface for Frequently-Bought-Together (FBT) recommendations — the "Customers also bought" carousel on the product detail page (PDP) and the cart.…