Wishlist API Reference
Complete API contracts for the Wishlist module, including routes, auth, DTOs, responses, errors, examples, and integration notes.
Wishlist - API Reference
Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers. Scope: The four customer-facing wishlist endpoints. No admin surface exists.
1. Documentation Evidence
| Area | Files Inspected | What Was Verified |
|---|---|---|
| Controllers | apps/api/src/modules/wishlist/customer/wishlist-customer.controller.ts | Routes, methods, guards, status codes |
| DTOs | customer/dto/*.ts | Query enums, validation |
| Services | customer/wishlist-customer.service.ts | Behavior, cache, errors |
| Schema | packages/db/src/schema/wishlist/wishlist-item.ts | Unique constraint, cascades, cap |
| Error registry | apps/api/src/common/types/error-codes.ts (// WISHLIST) | WISHLIST_* codes |
2. Module Summary
| Field | Value |
|---|---|
| Module name | wishlist |
| Module slug | wishlist |
| Primary actors | customer |
| API surfaces | mobile only |
| Base route prefixes | /api/mobile/wishlist |
| Auth model | JwtAuthGuard |
| Persistence | PostgreSQL (wishlist_item), Redis (30s membership cache) |
| Runtime source of truth | wishlist_item rows + live product rows |
| Sibling docs | Backend, Features and flows |
3. Concepts and Terminology
| Term | Meaning | Source File | Used By |
|---|---|---|---|
productId | The product public_id (uuid v7), never an integer | controllers | Save/remove |
savedAt | wishlist_item.created_at | schema | List |
status.available | Whether the product is buyable right now | service | List |
status.reason | Why an unavailable item is unavailable | service | List |
summary.totalItems | The whole wishlist count, not the page/filtered set | service | List |
pagination=false | Everything in one response, bounded at 200 | service | List |
4. API Surface Map
| Surface | Method | Path | Actor | Auth/Guard | Permission | Controller | Purpose |
|---|---|---|---|---|---|---|---|
| Mobile | GET | /api/mobile/wishlist | Customer | JWT + IpThrottle | — | WishlistCustomerController | List saved products |
| Mobile | GET | /api/mobile/wishlist/product-ids | Customer | JWT + IpThrottle | — | same | Membership id-set |
| Mobile | PUT | /api/mobile/wishlist/products/:productId | Customer | JWT + IpThrottle | — | same | Save (200, idempotent) |
| Mobile | DELETE | /api/mobile/wishlist/products/:productId | Customer | JWT + IpThrottle | — | same | Remove (200, never 404) |
5. Auth, Identity, and Permissions
| Surface | Guard/Decorator | Identity Shape | Permission | Guest Allowed | Notes |
|---|---|---|---|---|---|
| All | JwtAuthGuard, IpThrottlerGuard | req.user.id | — | No | Every query scoped to the account |
Rate limits, keyed on the account: CUSTOMER_READ 60/min (list, product-ids), CUSTOMER_WRITE 20/min (save, remove). No idempotency header is required — idempotency is structural (the unique constraint), not interceptor-based.
6. DTO and Model Reference
6.1 ListWishlistQueryDto
| Field | Type | Required | Default | Validation | Notes |
|---|---|---|---|---|---|
availability | enum | No | — | available | unavailable | Filter on buyability |
categoryId | string | No | — | UUID | |
brandId | string | No | — | UUID | |
search | string | No | — | inherited from QueryDto | |
sort | enum | No | savedAt | savedAt | name | price | availability | availability is a SQL sort on stored stock status, not a post-query shuffle |
order | enum | No | desc | asc | desc | |
pagination / page / size | — | No | true / 1 / 20 | inherited from QueryDto | pagination=false returns everything, bounded at 200 |
6.2 Params DTO
WishlistProductParamsDto { productId: string } — UUID v7.
6.3 Response DTOs
WishlistResponseDto:
{
"customer": { "id": "019fc692-…" },
"summary": { "totalItems": 42, "lastUpdated": "2026-08-05T10:00:00.000Z" },
"items": [
{ "id": "019fc692-…",
"savedAt": "2026-08-05T10:00:00.000Z",
"status": { "available": true, "reason": null },
"product": { "basic": {}, "pricing": {}, "classification": { "brand": {} },
"media": { "thumbnail": {} }, "status": {}, "inventory": {} } }
]
}WishlistProductIdsDto: { productIds: string[], totalItems: number }. WishlistItemDto: the item shape above.
Notes: product is the storefront card contract — literally the shape GET /products returns, so the component that renders a listing card renders a saved item unchanged, and anything added to the card contract appears here automatically. It is not the detail shape: no gallery, attributes, seo, tags, timestamps or sku.
status.lifecycle survives on the card, which matters here specifically — this is the one storefront surface that legitimately shows a product that can no longer be bought, and status beside it says why.
There is no top-level timestamps block — a wishlist is a set of items, not an entity.
7. Enum Reference
| Enum | Value | Meaning | Runtime Effect | Source |
|---|---|---|---|---|
WISHLIST_SORTS | savedAt / name / price / availability | List ordering | SQL sort with unique tiebreaker | dto |
WISHLIST_AVAILABILITY_FILTERS | available / unavailable | Buyability filter | Hides/shows unavailable items | dto |
reason (derived) | null / out_of_stock / draft / archived / removed | Why an item is unavailable | Precedence bottom-up: removed > archived > draft > out_of_stock > null | service |
8. Endpoint Reference
8.1 GET /api/mobile/wishlist
Purpose
The wishlist page. Paginated by default; every item resolves live product data. Items whose product was withdrawn, archived or sold out stay in the list with a reason.
Auth and Permissions
JwtAuthGuard, IpThrottlerGuard; CUSTOMER_READ 60/min (account-keyed).
Request
| Part | Required | Details |
|---|---|---|
| Query | No | availability, categoryId, brandId, search, sort, order, pagination, page, size |
Response
200 — envelope with data (WishlistResponseDto) + count/currentPage/totalPage when paginated.
{
"message": "Wishlist fetched successfully",
"errorCode": null,
"count": 42, "currentPage": 1, "totalPage": 3,
"data": {
"customer": { "id": "019fc692-…" },
"summary": { "totalItems": 42, "lastUpdated": "2026-08-05T10:00:00.000Z" },
"items": [
{ "id": "019fc692-…",
"savedAt": "2026-08-05T10:00:00.000Z",
"status": { "available": false, "reason": "archived" },
"product": { "basic": {}, "pricing": {}, "classification": { "brand": {} },
"media": { "thumbnail": {} }, "status": {}, "inventory": {} } }
]
}
}Side Effects
None — read-only; live product resolution via the shared inventory seam.
Error Cases
None beyond auth/rate-limit.
Edge Cases
Empty wishlist → items: [], totalItems: 0, lastUpdated: null. pagination=false → everything, bounded at 200. availability=available hides unavailable items; availability=unavailable lists exactly them.
8.2 GET /api/mobile/wishlist/product-ids
Purpose
One small payload so a listing page can render every heart icon without asking per tile. Use this for hearts on listing/search pages — fetching the full wishlist, or asking per tile, are both the wrong shape.
Response
200 — { productIds: [...], totalItems }. Cached server-side for 30 seconds; cleared immediately on the customer's own PUT/DELETE, so their writes are reflected at once, but another device can be up to 30s stale — update local state optimistically from your own writes.
8.3 PUT /api/mobile/wishlist/products/:productId
Purpose
Save a product. Idempotent — saving something already saved succeeds and changes nothing. Only a product currently published or unlisted can be saved; one already saved stays saved after it stops being either.
Auth and Permissions
CUSTOMER_WRITE 20/min (account-keyed).
Request
Empty body. :productId is the product public id (uuid v7).
Response
200 — never 201 — with the saved item. Identical whether the call created the row or found it, so a retry is safe and never looks like a failure.
Side Effects
wishlist_item insert (ON CONFLICT DO NOTHING); product-ids cache cleared.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 404 | WISHLIST_PRODUCT_NOT_FOUND | No such product, or one the customer may not save (draft, archived, deleted) — deliberately the same code for both, so hidden and nonexistent ids cannot be told apart |
| 409 | WISHLIST_LIMIT_REACHED | Already holds 200 saved products — re-saving an item already held succeeds even at the cap |
8.4 DELETE /api/mobile/wishlist/products/:productId
Purpose
Remove a product. Always 200 — removing something never saved, or a product id that names no product at all, is the outcome the caller asked for. DELETE never returns 404, deliberately (anti-enumeration).
Response
200 message-only.
Side Effects
Row hard-deleted; product-ids cache cleared.
Error Cases
None — by design. There is deliberately no WISHLIST_ITEM_NOT_FOUND.
9. Flow Diagrams
9.1 Route Ownership
9.2 Request Sequence (save)
9.3 Error Branch (save)
10. Pagination, Sorting, Filtering, and Search
| Endpoint | Pagination Type | Default Size | Max Size | Sort Fields | Filters | Result Cap |
|---|---|---|---|---|---|---|
GET /api/mobile/wishlist | offset page/size, paginated by default | 20 | 100 | savedAt (default), name, price, availability | availability, categoryId, brandId, search | 200 total (whole-list bound) |
Every sort carries a unique tiebreaker ((sortKey, id)-style) so offset paging over low-cardinality sorts never duplicates or omits items. summary.totalItems is the whole list, not the filtered set.
11. Caching, Jobs, and External Integrations
| Integration | Used? | Details |
|---|---|---|
| Redis cache | Yes | Membership id-set only — 30s (CACHE_TTL.VOLATILE), cleared on the customer's own writes; the list is never cached (must always show live data) |
| BullMQ | No | — |
| External API | No | — |
13. Mandatory Deep API Documentation Pack
13.1 Route-by-Route Completeness Matrix
| Route | Controller Method | DTOs | Service Method | Guards | Permissions | Cache | Jobs | DB Touches | Errors | Documented? |
|---|---|---|---|---|---|---|---|---|---|---|
GET /api/mobile/wishlist | list | ListWishlistQueryDto | WishlistCustomerService.list | JWT+IpThrottle | — | — | — | wishlist_item, products, inventory | — | Yes |
GET /api/mobile/wishlist/product-ids | productIds | — | …getProductIds | JWT+IpThrottle | — | 30s | — | wishlist_item | — | Yes |
PUT /wishlist/products/:productId | add | WishlistProductParamsDto | …add | JWT+IpThrottle | — | clear | — | wishlist_item, products | 404/409 | Yes |
DELETE /wishlist/products/:productId | remove | WishlistProductParamsDto | …remove | JWT+IpThrottle | — | clear | — | wishlist_item | — | Yes |
13.2 Request/Response Exhaustiveness
Covered in §8: empty-body requests (§8.3), success responses (§8.1/8.3), empty-list behavior (§8.1 edge cases), the unavailable-with-reason response (§8.1), domain errors (§8.3 error table), rate-limit behavior (account-keyed 429), the deliberate absence of a 404 on DELETE (§8.4).
13.3 API Diagram Pack
Route ownership (§9.1), sequence per endpoint family (§9.2, backend §7), error decision tree (§9.3), cache flow (backend §8), async/job flow — none (no jobs).
13.4 Consumer Integration Notes
| Consumer | Required Knowledge | Failure Handling | Contract Stability |
|---|---|---|---|
| Web frontend | product-ids for hearts, live product contract reuse, reason display | 404 on save of withdrawn product → refresh; never 404 on remove | Stable |
| Mobile app | 30s membership cache staleness across devices; optimistic local state from own writes | 429 → back off; PUT/DELETE retries safe (idempotent) | Stable |
| QA | Saveable-while-visible / stays-saved asymmetry, reason precedence | Reproduce via exact codes | Stable |
| Cart (future) | Reads stock from the same inventory seam — availability agrees by construction | — | Stable |
13.5 API Tradeoffs and Rationale
| Decision | Chosen Behavior | Alternatives Considered | Why This Tradeoff | Risk | Mitigation |
|---|---|---|---|---|---|
| PUT + unique constraint | Idempotent save, 200 both paths | POST + service check | Idempotency structural, cannot be forgotten | — | Concurrency spec |
| No soft delete | Hard delete | Tombstone + partial unique | Re-add stays one insert | No history | Accepted |
| DELETE never 404s | Always 200 | 404 on unknown | Anti-enumeration | Idempotent surprise | Documented |
| Live data on read | Never snapshotted | Snapshot on save | Always current price/stock | Read cost | Batched seam |
| 30s membership cache | Short TTL | 300s | Fill race bounds staleness | Cross-device lag | Cleared on own writes |
| 200-item cap | Bounded reads | Unbounded | Abuse bound | Rare overflow | Approximate under concurrency |
13.6 API Change Impact
| Change | Affected Consumers | Backend Impact | Data Impact | Migration Needed? | Compatibility Plan |
|---|---|---|---|---|---|
ProductCustomerStatusDto.lifecycle widened | Clients with exhaustive switches | DTO union | None | Compile-time only | Runtime no-op — discovery surfaces still emit only published |
14. Zero-Omission API Checklist
- Every controller route is documented (§4, §8, §13.1).
- Every parent route prefix and runtime URL is documented (§2, §4).
- Every DTO field, enum, default, transform and validator is documented (§6, §7).
- Every response field and nullable field is documented (§6.3, §8).
- Every auth, guard, permission and guest identity branch is documented (§5).
- Every success, validation, not-found, conflict, rate-limit and server-error branch is documented (§8).
- Every DB read/write, cache hit/miss/invalidation and external call is documented (§11, backend §8).
- Every route has examples for success responses and representative failures (§8).
- Every endpoint family has route, sequence and error diagrams (§9, backend §7).
- Every tradeoff and compatibility risk is documented (§13.5, §13.6).
- The API doc links to backend and features/flows (§1, See Also).
15. Integration Checklist
- Every route from controllers is documented.
- Every DTO field is documented.
- Every enum value is documented.
- Every response envelope is documented.
- Every error code is documented.
- Every auth guard and permission is documented.
- Every cache key, queue job and external call is documented.
- Every diagram matches the current code.
- The API doc links to backend and features/flows.
See Also
- Backend doc: /docs/developer/wishlist/backend
- Features and flows doc: /docs/developer/wishlist/feature
- TDD: not yet published