Catalog Module — Storefront
HTTP surface for unauthenticated catalog reads. The storefront uses these endpoints to render the navigation tree, brand / tag / ingredient pages, the product detail page (PDP),…
HTTP surface for unauthenticated catalog reads. The storefront uses these endpoints to render the navigation tree, brand / tag / ingredient pages, the product detail page (PDP), and to resolve slugs to ids for deep-linking. Only active, non-deleted rows are visible — admin and vendor product/variant management lives in sibling docs.
Source:
api-modules/catalog/src/controllers/public-catalog.controller.ts(taxonomy reads),api-modules/catalog/src/controllers/store-product.controller.ts(product detail).The four taxonomies (categories, brands, tags, ingredients) share a single response shape (
CatalogItemResponse). Cart, order, and inventory consume product+variant data via this module's services; HSN classification lives onproduct_variant, not the product, because variants in the same family can carry different HSN codes.
Conventions
Authentication
| Endpoint group | Auth |
|---|---|
GET /store/catalog/** | none (public) |
GET /store/products/:slug | none (public) |
No session is required. Soft-deleted (deletedAt IS NOT NULL) and inactive (isActive = false) rows are filtered server-side; the storefront never sees them.
Response envelope
Successful responses are wrapped by ResponseInterceptor:
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": { /* pagination on list endpoints */ }
}Paginated lists use the standard metadata: { total, limit, offset, hasMore } shape.
Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR (bad query coercion) |
| 404 | NOT_FOUND |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
QueryDto
List endpoints accept the platform's standard QueryDto:
| Name | Type | Default | Notes |
|---|---|---|---|
searchValue | string? | — | Combined with searchField + searchOperator (contains / starts_with / ends_with) |
searchField | string? | — | Allowed fields are per-service (typically title, slug) |
limit | int | 100 | 1..500 |
offset | int | 0 | ≥ 0 |
sortBy, sortDirection | string?, asc/desc | —, desc | Allowed sort fields are per-service |
filters[] | JSON? | — | [{ field, value, operator }]; operator is one of eq, ne, lt, lte, gt, gte, in, not_in, contains, starts_with, ends_with |
Domain types
CatalogItemResponse
Shared by brand / category / tag / ingredient.
type CatalogItemResponse = {
id: string;
title: string;
description: string | null;
slug: string; // lowercase alnum + hyphens
image: string | null;
metadata: Record<string, unknown> | null;
isActive: boolean; // always true for storefront reads
createdAt: string; // ISO
updatedAt: string;
deletedAt: string | null; // always null for storefront reads
};Categories
Base path: /store/catalog/categories.
GET /store/catalog/categories — List active categories
Paginated. Accepts the standard QueryDto.
Response 200 — paginated CatalogItemResponse[].
GET /store/catalog/categories/tree — Active category tree
Returns the full parent/child hierarchy in a single call, materialized server-side. Inactive and soft-deleted categories (and their subtrees) are pruned. Not paginated.
Response 200 — array of category nodes with nested children: CategoryNode[] per the category service's tree shape.
GET /store/catalog/categories/slug/:slug — Get a category by slug
Path params
| Name | Notes |
|---|---|
slug | Lowercase alnum + hyphens. The service filters to isActive=true, deletedAt IS NULL |
Response 200 — CatalogItemResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | No active category with that slug |
GET /store/catalog/categories/:id — Get a category by id
Response 200 — CatalogItemResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Id does not exist, or row is inactive / soft-deleted |
Brands
Base path: /store/catalog/brands. Same shape as categories — minus the tree endpoint.
GET /store/catalog/brands — List active brands
Paginated QueryDto. Response 200 — paginated CatalogItemResponse[].
GET /store/catalog/brands/slug/:slug
Response 200 — CatalogItemResponse. 404 if the brand is missing / inactive / deleted.
GET /store/catalog/brands/:id
Response 200 — CatalogItemResponse. 404 if the brand is missing / inactive / deleted.
Tags
Base path: /store/catalog/tags. Same shape as brands.
GET /store/catalog/tags — List active tags
Paginated QueryDto. Response 200 — paginated CatalogItemResponse[].
GET /store/catalog/tags/slug/:slug
Response 200 — CatalogItemResponse. 404 if the tag is missing / inactive / deleted.
GET /store/catalog/tags/:id
Response 200 — CatalogItemResponse. 404 if the tag is missing / inactive / deleted.
Ingredients
Base path: /store/catalog/ingredients. Same shape as brands.
GET /store/catalog/ingredients — List active ingredients
Paginated QueryDto. Response 200 — paginated CatalogItemResponse[].
GET /store/catalog/ingredients/slug/:slug
Response 200 — CatalogItemResponse. 404 if the ingredient is missing / inactive / deleted.
GET /store/catalog/ingredients/:id
Response 200 — CatalogItemResponse. 404 if the ingredient is missing / inactive / deleted.
Products (PDP)
Base path: /store/products. The public product-detail surface that backs the storefront PDP. Listing and faceted discovery live in search.md; this endpoint is the single-product hydrate once a slug is known.
GET /store/products/:slug — Product detail by slug
Returns a store-visible product with its options, variants, content tabs, taxonomy id arrays, and a slim vendor summary. Visibility gating — only a product that is status=active, visibility=public, non-deleted, and published (publishedAt IS NOT NULL AND publishedAt <= now()) is returned; anything else is a 404 (the same neutral 404 as an unknown slug, so unpublished/hidden products can't be probed).
Path params
| Name | Notes |
|---|---|
slug | Product slug. The public PDP URL is /product/{slug} (singular) on the storefront; this API path is store/products/:slug. |
Response 200 — StoreProductDetail:
type StoreVendorSummary = {
id: string;
name: string;
slug: string;
logo: string | null;
};
type StoreProductDetail = {
product: ProductRecord; // core product columns (title, subtitle,
// description, brandId, thumbnail, images,
// SEO meta, hsCode, etc.)
options: ProductOptionDetail[]; // option groups + values (e.g. Size, Color)
variants: ProductVariantDetail[]; // purchasable variants: price/specialPrice
// (subunits), sku/ean/upc/barcode, hsnCode,
// min/max per cart, option-value refs
tabs: TabRecord[]; // rich-content tabs (description, how-to, etc.)
categoryIds: string[];
tagIds: string[];
ingredientIds: string[];
vendor: StoreVendorSummary;
};The product / options / variants / tabs sub-shapes are identical to the ProductDetail base returned by the admin/vendor product-detail endpoints — see ../admin/catalog.md for the full field-level breakdown. The storefront variant differs only in the gating above and the slim StoreVendorSummary (no admin-only vendor fields). Prices on variants are integer subunits.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | No product with that slug, or it is inactive / non-public / unpublished / soft-deleted, or its vendor cannot be resolved |
Related modules
search— actual product listing and faceting for the storefront. This module only exposes taxonomy reads + single-product detail; product search/filter lives insearch.md.banner—GET /store/banners/:entityType/slug/:slugaccepts the sameslugvalues exposed by this surface. Seebanner.md.cart— variants added to the cart come from this module's product+variant tables.
Cart Module — Storefront
HTTP surface for the storefront shopping cart — guest and logged-in reads/mutates, address attach, coupon apply/remove + browse, free-gift picker, guest→customer merge on login,…
Customer Module — Storefront
HTTP surface for the customer-side address book. The customer identity itself lives in Better-Auth's user table; this module owns the shopping-related customer data on top of that…