Wishlist Module — Storefront
HTTP surface for the customer wishlist — one wishlist per customer ("My Wishlist"), keyed at the variant level. The default wishlist is lazy-materialized on first access so the…
HTTP surface for the customer wishlist — one wishlist per customer ("My Wishlist"), keyed at the variant level. The default wishlist is lazy-materialized on first access so the storefront never has to call a "create my wishlist" endpoint. The wishlist itself is invisible on the wire: there is no list id in the URL or response — leaves room for a future multi-list surface at a sibling path without breaking this contract.
Source:
api-modules/wishlist/src/controllers/store-wishlist.controller.ts.This is a customer-only surface — there is no admin or vendor controller. The wishlist module hooks into the cart module for the "add to cart" action; cart errors propagate unchanged.
Conventions
Authentication
| Endpoint group | Auth |
|---|---|
GET/POST/DELETE /store/wishlist/** | required (customer session) |
Every read and write is scoped to session.user.id. There is no guest wishlist.
Headers
The add-to-cart endpoint accepts the cart module's headers (forwarded verbatim to the cart layer):
| Header | Direction | Notes |
|---|---|---|
x-cart-token | request (optional) + response | Cart handle. Mint by omitting — response sets it. |
x-platform | request (optional) | WEB / APP (case-insensitive). Defaults to WEB. |
Response envelope
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": { /* on the list endpoint */ }
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR |
| 401 | UNAUTHORIZED |
| 404 | NOT_FOUND |
| 409 | INSUFFICIENT_INVENTORY, BELOW_MIN_QUANTITY_PER_CART, ABOVE_MAX_QUANTITY_PER_CART (from cart-side validation during add-to-cart) |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Currency
Price fields on WishlistItemResponse (currentPrice, regularPrice, specialPrice) are integer subunits.
Domain types
WishlistItemResponse
type WishlistItemResponse = {
variantId: string;
productId: string;
vendorId: string;
productName: string;
variantName: string | null;
thumbnail: string | null;
/** Effective unit price (active special if in-window, else regular). Integer subunits. */
currentPrice: number | null;
regularPrice: number | null;
/** Active special unit price, or null if none / outside the window. */
specialPrice: number | null;
/** Best-effort live stock signal. `false` reflects present-but-OOS — soft-deleted variants
* are filtered out of the response entirely. */
isInStock: boolean;
addedAt: string; // ISO
};The shape mirrors the cart-line snapshot fields so storefronts can reuse a single product-card rendering path.
WishlistContainsResponse
type WishlistContainsResponse = {
membership: Record<string, boolean>; // keyed by variantId; every requested id is present
};Endpoints
Base path: /store/wishlist.
GET /store/wishlist — List my wishlist
Paginated. Newest-added first. The default wishlist is materialized on first call. Soft-deleted variants are filtered out of the response (the underlying row stays in case the variant is restored).
Query
| Name | Type | Default | Constraints |
|---|---|---|---|
page | int | 1 | >= 1 |
limit | int | DEFAULT_WISHLIST_PAGE_SIZE | 1..MAX_WISHLIST_PAGE_SIZE |
Response 200 — paginated WishlistItemResponse[] with metadata: { total, limit, offset, hasMore }.
POST /store/wishlist/items — Add a variant
Idempotent. Adding the same variant again returns the existing entry and does not re-emit wishlist.item.added.
Body
{ "variantId": "01J9..." }| Field | Constraints |
|---|---|
variantId | string, min 1 char |
Response 201 — WishlistItemResponse (same hydrated shape as GET /store/wishlist entries — no follow-up fetch needed).
Side effects — emits wishlist.item.added on first add (not on idempotent re-add).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod |
| 404 | NOT_FOUND | Variant does not exist |
DELETE /store/wishlist/items/:variantId — Remove a variant
Idempotent — removing an absent variant still returns 204.
Response 204 No Content.
Side effects — emits wishlist.item.removed when a row was actually removed.
DELETE /store/wishlist/items — Clear my wishlist
Removes every item from the caller's wishlist.
Response 204 No Content.
POST /store/wishlist/items/:variantId/add-to-cart — Move a wishlist item into the cart
Delegates to the cart's add-line. The wishlist entry is NOT removed — the customer keeps the variant saved until they explicitly remove it (rationale: "added to cart" doesn't mean "no longer wanted later"). Cart errors (out-of-stock, min/max quantity, vendor disabled, etc.) propagate unchanged.
Path params
| Name | Notes |
|---|---|
variantId | Variant id to add to cart |
Query
| Name | Type | Default | Notes |
|---|---|---|---|
quantity | int | 1 | >= 1. Forwarded verbatim to CartLineService.addLine |
Headers
| Header | Notes |
|---|---|
x-cart-token | Optional. Mint on first request. Response always emits the active cart's token. |
x-platform | Optional. WEB / APP. |
Response 200 — CartResponse (the cart module's full cart shape — see cart.md).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR, BELOW_MIN_QUANTITY_PER_CART, ABOVE_MAX_QUANTITY_PER_CART | Cart-side validation |
| 404 | NOT_FOUND | Variant does not exist |
| 409 | INSUFFICIENT_INVENTORY | Stock cannot cover the requested quantity |
POST /store/wishlist/contains — Bulk-check membership
Bulk-only — the singular /contains/:variantId form is intentionally absent so a product-listing page rendering N cards hits the server once, not N times. Does NOT lazy-create the default wishlist; if the caller has never wished for anything, every returned key is false.
Body
{ "variantIds": ["01J9...", "01J9..."] }| Field | Constraints |
|---|---|
variantIds | array, 1..MAX_WISHLIST_CONTAINS_VARIANT_IDS entries; each min 1 char |
Response 200
{
"data": {
"membership": {
"01J9aaaa...": true,
"01J9bbbb...": false
}
}
}Every variantId from the request is present in membership — clients can index without a hasOwnProperty check.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Empty array, too many ids, etc. |
Related modules
cart—add-to-cartforwards to the cart's add-line; the response is the cart'sCartResponse. Seecart.md.catalog— variants referenced byvariantIdcome from this module.inventory— drives theisInStocksignal on each row.
Shipping Module — Storefront
HTTP surface for customer-side shipment tracking — the timeline of provider-emitted events for a sub-order the customer placed. Read-only.
Admin RBAC Module
HTTP surface for managing platform-admin roles (custom role definitions with a permission map) and reading the catalog of available permissions. Also exports the PermissionsGuard…