Search Module — Storefront
HTTP surface for the storefront product search — Typesense-backed faceted search and autocomplete suggestions. Mirrors the beautybarn response shape so the catalog/listing screens…
HTTP surface for the storefront product search — Typesense-backed faceted search and autocomplete suggestions. Mirrors the beautybarn response shape so the catalog/listing screens have a near drop-in surface during the re-platform.
Source:
api-modules/search/src/controllers/store-product-search.controller.ts.The admin reindex/backfill surface lives in
docs/separated/admin/search.md.
Conventions
Authentication
| Endpoint | Auth |
|---|---|
GET /store/product-search | none (public) |
GET /store/product-search/suggestions | none (public) |
Response envelope
The search and suggestions endpoints embed their own data key so the wire envelope is the standard { data, metadata?, message, statusCode }:
{
"data": { /* products + facets, or suggestions + products */ },
"metadata": { /* on /store/product-search only */ },
"message": "Success",
"statusCode": 200
}The metadata uses the named-pagination shape (total / items / perPage / currentPage / lastPage) rather than the offset-based total / limit / offset / hasMore shape — search is page-based.
Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR (malformed attributes JSON, bad price coercion) |
| 500 | INTERNAL_SERVER_ERROR, SEARCH_UPSTREAM_ERROR |
Currency
All variant price fields (price, specialPrice, originalPrice, currentPrice, specialPriceActive, priceStart, priceEnd, minPrice/maxPrice query) are integer subunits (paise / cents / eurocents).
Domain types
ProductSearchVariant
type ProductSearchVariant = {
id: string;
sku: string | null;
price: number | null;
specialPrice: number | null;
specialPriceStartDate: string | null; // ISO
specialPriceEndDate: string | null; // ISO
inventoryQuantity: number;
minQuantityPerCart: number | null;
maxQuantityPerCart: number | null;
thumbnail: string | null;
images: string[];
/** Computed at response time from the fields above. */
originalPrice: number | null; // = price
currentPrice: number | null; // = specialPriceActive ?? price
specialPriceActive: number | null; // specialPrice if window currently includes now, else null
};ProductSearchProduct
type ProductSearchProduct = {
id: string;
title: string;
subtitle: string | null;
description: string | null;
slug: string; // (beautybarn called this "handle")
thumbnail: string | null;
images: string[];
/** min/max of currentPrice across in-stock variants; null when no variant is priced. */
priceStart: number | null;
priceEnd: number | null;
brand: { id: string; slug: string; name: string } | null;
inStock: boolean;
hasActiveSpecial: boolean;
variants: ProductSearchVariant[];
};Facets
type ProductSearchBrandFacet = {
id: string;
slug: string;
name: string;
productCount: number;
};
type ProductSearchIngredientFacet = {
id: string;
slug: string;
name: string;
productCount: number;
};
type ProductSearchAttributeFacet = {
code: string;
title: string;
values: Array<{ value: string; productCount: number }>;
};Endpoints
GET /store/product-search — Faceted product search
Returns paginated products plus three facet blocks (brands, ingredients, attributes) computed against the same filtered set so client-side facet UI counts stay accurate.
Query
| Name | Type | Default | Notes |
|---|---|---|---|
q | string? | — | Trimmed, max 200 chars. Full-text query against title + description. |
brands | csv string? | — | Comma-separated brand slugs (or ids — depends on the index field). |
categories | csv string? | — | Comma-separated category slugs/ids. |
tags | csv string? | — | Comma-separated tag slugs/ids. |
ingredients | csv string? | — | Comma-separated ingredient slugs/ids. |
skus | csv string? | — | Comma-separated SKUs (variant-level). |
attributes | JSON-encoded string? | — | { "<attribute-code>": "value" | string[] }. Malformed JSON or non-string values returns 400. |
minPrice | int? | — | Subunit lower bound (inclusive). |
maxPrice | int? | — | Subunit upper bound (inclusive). |
inStock | "true" | "false"? | — | Coerced to boolean. When omitted, no filter is applied. |
hasActiveSpecial | "true" | "false"? | — | Coerced to boolean. |
page | int | 1 | 1..1000 |
limit | int | DEFAULT_SEARCH_LIMIT | 1..MAX_SEARCH_LIMIT |
sortBy | enum? | relevance (implicit) | One of relevance / price-asc / price-desc / new / best-selling / inventory-high / inventory-low |
Response 200
{
"data": {
"products": [
{
"id": "01J9...",
"title": "Hydrating Toner",
"subtitle": "200ml",
"description": "...",
"slug": "hydrating-toner",
"thumbnail": "https://cdn.example/p/hydrating-toner.jpg",
"images": ["https://cdn.example/p/hydrating-toner-2.jpg"],
"priceStart": 49900,
"priceEnd": 79900,
"brand": { "id": "01J9...", "slug": "skin-co", "name": "Skin Co" },
"inStock": true,
"hasActiveSpecial": false,
"variants": [ /* ProductSearchVariant[] */ ]
}
],
"brands": [ { "id": "01J9...", "slug": "skin-co", "name": "Skin Co", "productCount": 12 } ],
"ingredients": [ { "id": "01J9...", "slug": "niacinamide", "name": "Niacinamide", "productCount": 8 } ],
"attributes": [
{ "code": "size", "title": "Size", "values": [ { "value": "200ml", "productCount": 5 } ] }
]
},
"metadata": {
"total": 42,
"items": 20,
"perPage": 20,
"currentPage": 1,
"lastPage": 3
},
"message": "Success",
"statusCode": 200
}GET /store/product-search/suggestions — Autocomplete
Returns distinct title suggestions plus a small set of matching products for an inline search dropdown.
Query
| Name | Type | Default | Notes |
|---|---|---|---|
q | string | — required | Trimmed, 2..100 chars |
limit | int | DEFAULT_SUGGESTIONS_LIMIT | 1..20 |
Response 200
{
"data": {
"suggestions": ["Hydrating Toner", "Hydrating Serum"],
"products": [ /* ProductSearchProduct[] */ ]
},
"message": "Success",
"statusCode": 200
}Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | q < 2 chars |
Related modules
catalog— products and variants returned here are reindexed from the catalog module's writes.inventory—inStock/inventoryQuantityreflect the inventory module's current view (with a small replication lag — the search index is eventually consistent).product-attribute—attributesfacet values come from the product-attribute module.
Rewards Module — Storefront
HTTP surface for the customer-facing rewards/loyalty plugin. Customers earn points on three actions (account registration, product purchase, product review) and redeem them at the…
Settings Module — Storefront
HTTP surface for the storefront to read public, platform-wide settings (branding, contact details, currency hints, store toggles, etc.). The whole storefront-public configuration…