Cart API Reference
Complete API contracts for the Cart module, including routes, auth, DTOs, responses, errors, examples, and integration notes.
Cart - API Reference
Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers. Scope: The eight customer cart routes and the two read-only admin routes.
1. Documentation Evidence
| Area | Files Inspected | What Was Verified |
|---|---|---|
| Controllers | apps/api/src/modules/cart/customer/cart-customer.controller.ts, admin/cart-admin.controller.ts | Routes, methods, guards, status codes, interceptor wiring |
| DTOs | customer/dto/*.ts, admin/dto/*.ts | Validation, defaults, query enums |
| Services | cart-write.service.ts, cart-bulk.service.ts, cart-query.service.ts | Behavior, errors, version semantics |
| Schema | packages/db/src/schema/cart/*.ts | Partial unique, caps, status enum |
| Error registry | apps/api/src/common/types/error-codes.ts (// CART) | CART_* codes |
2. Module Summary
| Field | Value |
|---|---|
| Module name | cart |
| Module slug | cart |
| Primary actors | customer, admin (read-only) |
| API surfaces | mobile (customer), admin |
| Base route prefixes | /api/mobile/cart, /api/admin/carts |
| Auth model | JwtAuthGuard (customer); JwtAuthGuard + RoleGuard (Cart_READ, admin) |
| Persistence | PostgreSQL (cart, cart_item), Redis (30s summary cache) |
| Runtime source of truth | cart/cart_item rows + live product/inventory rows |
| Sibling docs | Backend, Features and flows |
3. Concepts and Terminology
| Term | Meaning | Source File | Used By |
|---|---|---|---|
version | Optimistic concurrency token; optional on single mutations, required on bulk; every response carries it | schema | All writes |
status | active (only one the module writes), checkout_locked, converted | schema | Mutations, admin list |
totalQuantity | Sum over every line, valid or not — the badge number | builder | Summary, cart |
subtotal | Valid lines only (customer); every line (admin) | builder | Pricing |
lastKnownUnitPrice / last_known_unit_price | Not a snapshot — "has this changed since you chose it" signal | schema | priceChange |
Idempotency-Key | Optional on POST /items only | interceptor | Add |
checkout_locked | Absorbing state until checkout owns the return edge | schema | Mutations |
4. API Surface Map
| Surface | Method | Path | Actor | Auth/Guard | Permission | Controller | Purpose |
|---|---|---|---|---|---|---|---|
| Mobile | GET | /api/mobile/cart | Customer | JWT + IpThrottle | — | CartCustomerController | My cart |
| Mobile | GET | /api/mobile/cart/summary | Customer | JWT + IpThrottle | — | same | Badge counters |
| Mobile | GET | /api/mobile/cart/checkout-validation | Customer | JWT + IpThrottle | — | same | Strict validation pass |
| Mobile | POST | /api/mobile/cart/items | Customer | JWT + IpThrottle | — | same | Add (delta) |
| Mobile | POST | /api/mobile/cart/items/bulk | Customer | JWT + IpThrottle | — | same | Batch edit |
| Mobile | PUT | /api/mobile/cart/items/:variantPublicId | Customer | JWT + IpThrottle | — | same | Set quantity (absolute) |
| Mobile | DELETE | /api/mobile/cart/items/:variantPublicId | Customer | JWT + IpThrottle | — | same | Remove line |
| Mobile | DELETE | /api/mobile/cart/items | Customer | JWT + IpThrottle | — | same | Empty cart |
| Admin | GET | /api/admin/carts | Admin | JWT + Role | Cart_READ | CartAdminController | Read-only list |
| Admin | GET | /api/admin/carts/:cartId | Admin | JWT + Role | Cart_READ | same | Read-only detail |
{variantPublicId}, {productId} and {cartId} are public uuids (v7), never integers. Literal segments (summary, checkout-validation, items, bulk) are declared before any :param-shaped route on the same verb.
5. Auth, Identity, and Permissions
| Surface | Guard/Decorator | Identity Shape | Permission | Guest Allowed | Notes |
|---|---|---|---|---|---|
| Customer | JwtAuthGuard, IpThrottlerGuard | req.user.id | — | No | Every query scoped to the account; guest carts are structurally impossible (customer_id NOT NULL) |
| Admin | JwtAuthGuard, RoleGuard, IpThrottlerGuard | req.user | Cart_READ | No | Read-only |
Rate limits: customer reads CUSTOMER_READ 60/min (account-keyed); customer mutations CUSTOMER_CART_MUTATION 60/min (account-keyed, three times the ordinary write limit — a stepper on several lines produces a legitimate burst); admin 30/min IP. Idempotency-Key: optional, POST /items only (scope cart-item-add).
6. DTO and Model Reference
6.1 AddCartItemDto
| Field | Type | Required | Default | Validation | Notes |
|---|---|---|---|---|---|
productId | string | Yes | N/A | UUID v7 | |
variantPublicId | string | No | N/A | UUID v7 | The configuration to add. Omitted means the product's default variant. A variant belonging to another product is refused with CART_PRODUCT_NOT_FOUND. |
quantity | number | Yes | N/A | int 1..99 | Delta — adds to the existing line |
version | number | No | — | >= 1 | Optional |
6.2 SetCartItemQuantityDto
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
quantity | number | Yes | int 0..99 | Absolute; 0 removes |
version | number | No | >= 1 | Optional |
6.3 BulkCartMutationDto
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
version | number | Yes | >= 1 | Required here — only a client that loaded the cart sends a batch |
operations | array | Yes | max 50; no duplicate LINE — see below | Applied in order, one transaction, one version bump |
Operation shapes: { op: "set", productId, variantPublicId?, quantity: 0..99 } (absolute, 0 removes),
{ op: "add", productId, variantPublicId?, quantity: 1..99 } (delta), { op: "remove", productId, variantPublicId? }.
variantPublicId is optional on every operation including remove, and omitting it always means
the product's default variant.
Two variants of one product in one batch is legal and is the point. What is refused is naming the
same LINE twice, and that check runs twice: once on what the client wrote, where {productId: P}
and {productId: P, variantPublicId: P's default} are two different keys, and again after resolution,
where they are one variant_id. Sending both is CART_BULK_DUPLICATE_PRODUCT. Pick one convention per
client — always send the variant, or never send it for single-variant products — and stay with it.
6.4 Query DTOs
CartVersionQueryDto { version? } — used on the two DELETEs, where the concurrency token travels as a query parameter (a body on DELETE is not universally survivable). Admin list: status, customerId, inactiveForDays, createdFrom, createdTo (date-only createdTo is inclusive of the whole day), sort (lastActivityAt default, createdAt, updatedAt, itemCount), order, page, size — no pagination parameter; sending one is a 400.
6.5 Response DTOs
CartResponseDto (whole cart, returned by every mutation):
{
"cart": { "id": "019fc6…", "status": "active", "version": 7 },
"customer": { "id": "019fc6…" },
"summary": { "totalItems": 3, "totalQuantity": 7, "validItems": 2, "readyForCheckout": false },
"items": [ /* see below */ ],
"pricing": { "subtotal": 249900, "savings": 30000, "currency": "NPR", "excludedItemCount": 1 },
"validation": { "blockingReasons": ["items_unavailable"] },
"timestamps": { "createdAt": "…", "updatedAt": "…", "lastActivityAt": "…" }
}A customer with no cart gets "cart": null, "items": [], zeroed counters, "timestamps": null — GET never creates a cart.
A cart line:
{
"id": "019fc6…", "quantity": 2, "addedAt": "…", "updatedAt": "…",
"product": { "basic": {}, "pricing": {}, "classification": { "brand": {} },
"media": { "thumbnail": {} }, "status": {}, "inventory": {} },
"pricing": { "unitPrice": 124950, "lineSubtotal": 249900,
"unitMrp": 139950, "lineSavings": 30000, "currency": "NPR",
"deal": { "basePrice": 149950, "effectivePrice": 124950,
"saving": 25000, "discountBps": 1667,
"effectiveMaxPrice": null } },
"priceChange": { "changed": true, "previousPrice": 119950, "currentPrice": 124950 },
"validation": { "valid": false, "reason": "insufficient_stock",
"requestedQuantity": 2, "availableQuantity": 1 }
}What pricing.unitPrice is, and what it is not
unitPrice is the LINE'S OWN VARIANT price, with any live special-deal campaign already
applied. It is not product.pricing.sellingPrice, which is the product ROLLUP — the minimum
across the product's sellable variants.
Never total, charge, or display a line price from product.pricing.sellingPrice.
Always use pricing.unitPrice / pricing.lineSubtotal.The failure this prevents is silent and complete: a customer who chose the 512GB variant would be billed the 128GB price, and the checkout freeze, the order line and the invoice would all agree with the undercharge, because every one of them takes its figure from this field.
pricing.deal is the campaign applied to this line, or null. unitPrice already carries it —
deal exists so the customer can be shown what came off and why. Its effectiveMaxPrice is always
null on a cart line: a line is one variant, so there is no band.
lineSavings is measured from the MRP, so a variant markdown and a campaign discount arrive as
one "you saved" figure rather than two a client might add together.
The card embedded at product has its pricing.effectivePrice and pricing.deal overwritten
with this line's figures, so either is safe to render here. Its sellingPrice, mrp,
maxSellingPrice and isPriceRange remain the product rollup — they are the "from" price and the
struck-through was-price, and they are never charged.
product is the storefront card contract — the same shape GET /products returns, not the detail shape. A cart line renders a thumbnail, a name, a price and a stock state, so it carries basic (id/name/slug), pricing, classification.brand, media.thumbnail, status and inventory, and nothing else. There is no gallery, no attributes, no seo, no tags, no timestamps and no sku; read GET /products/{slug} for those.
sku is absent on purpose and it is not missing from the purchase record: the order line still snapshots it, read from the product row under the cart lock in CheckoutAccessService.loadLineIdentity. What a purchase says was bought must not be sourced from a display response.
item.id is for list keying only — no route accepts it; lines are addressed by item.variant.id,
which is product_variant.public_id.
A cart line is (cart, variant), enforced by uq_cart_item_cart_id_variant_id. Two configurations of
one product are two independent lines, each charged from its own variant's price with its own campaign
applied. Every item therefore carries a variant block:
| Field | Type | Notes |
|---|---|---|
variant.id | string | product_variant.public_id. The line's address — the path segment of the set-quantity and remove routes. |
variant.name | string | null | null when the variant IS the product; render the product name alone. |
variant.sku | string | null | The variant's own SKU. |
pricing.unitPrice is what is charged. product.pricing.sellingPrice is the product ROLLUP — the
minimum across sellable variants — correct for a listing card's "from Rs X" and an undercharge on a
line. The embedded card's effectivePrice and deal are overwritten with this line's figures; every
other field under product.pricing stays the rollup.
CartCountsDto: { totalItems, totalQuantity } (no version, deliberately). CheckoutValidationDto: readyForCheckout + cart + product/inventory/pricing groups + blockingReasons. Admin row: { id, status, version, customer: { id, name }, itemCount, totalQuantity, subtotal, currency, createdAt, updatedAt, lastActivityAt }; detail adds lines with a three-field product reference.
7. Enum Reference
| Enum | Value | Meaning | Runtime Effect | Source |
|---|---|---|---|---|
cart_status | active | The only value the module writes | Mutations require it | enums.ts |
cart_status | checkout_locked | Checkout in flight | Refuses all mutations (CART_LOCKED_FOR_CHECKOUT) | |
cart_status | converted | Order created | Excluded from the live-cart partial unique | |
validation.reason (derived) | null / removed / archived / draft / out_of_stock / insufficient_stock | Why a line is invalid | Top-down precedence: removed first | builder |
blockingReasons | empty / cart_locked / items_unavailable / insufficient_stock | Checkout blockers | Subset of the four | query |
8. Endpoint Reference
8.1 GET /api/mobile/cart
Purpose
The cart page. Never cached, never paginated; every line resolves live product, price and stock. Lines whose product was withdrawn, archived or sold out stay with a reason and are excluded from the subtotal.
Auth and Permissions
JwtAuthGuard, IpThrottlerGuard; CUSTOMER_READ 60/min (account-keyed).
Response
200 — CartResponseDto. Empty cart for a customer who never had one — nothing is created.
Error Cases
None beyond auth/rate-limit.
8.2 GET /api/mobile/cart/summary
Purpose
Badge counters. Two integers, cached 30 seconds, cleared immediately by the customer's own writes. Deliberately carries no version/status.
Response
200 — { totalItems, totalQuantity }. totalQuantity counts every line, valid or not — it must not drop when a product goes out of stock.
8.3 GET /api/mobile/cart/checkout-validation
Purpose
Can checkout start? Re-reads product, price and stock and trusts nothing previously loaded — that re-read is the point. Shipping, promotions and address validation are deliberately absent.
Response
200 — CheckoutValidationDto with readyForCheckout, the re-checked groups and blockingReasons ⊆ empty, cart_locked, items_unavailable, insufficient_stock.
8.4 POST /api/mobile/cart/items
Purpose
Add a product — delta, the one non-idempotent verb, for the product card that cannot know the absolute target. Only a currently published or unlisted product can be added; being out of stock does NOT block the add — it is reported on the line and blocks checkout.
Auth and Permissions
CUSTOMER_CART_MUTATION 60/min (account-keyed). Idempotency-Key optional — retry with the same key replays the stored response instead of adding again.
Request
{ "productId": "019fc6…", "variantPublicId": "019fc6…", "quantity": 2, "version": 7 }Response
200 — whole cart.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 400 | validation | quantity > 99 in a single request |
| 404 | CART_PRODUCT_NOT_FOUND | No such product, one that may not be added (draft, archived, deleted), no such variant, or a variant belonging to a different product — one code covers all four deliberately; telling them apart would let a caller enumerate unpublished product/variant ids |
| 409 | CART_LOCKED_FOR_CHECKOUT | Cart locked by checkout |
| 409 | CART_VERSION_CONFLICT | Stale version |
| 409 | CART_ITEM_LIMIT_REACHED | 50 distinct products |
| 409 | CART_QUANTITY_LIMIT_EXCEEDED | Resulting line quantity > 99 by accumulation |
8.5 POST /api/mobile/cart/items/bulk
Purpose
Apply several changes at once — what a debouncing client flushes after multi-line edits. One transaction, one version bump, applied in order. version required; no Idempotency-Key path.
Request
{
"version": 7,
"operations": [
{ "op": "set", "productId": "019fc6…", "variantPublicId": "019fc6…", "quantity": 6 },
{ "op": "add", "productId": "019fc6…", "variantPublicId": "019fc6…", "quantity": 2 },
{ "op": "remove", "productId": "019fc6…" }
]
}Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 400 | CART_BULK_DUPLICATE_PRODUCT | Same product named twice |
| 400 | CART_VERSION_CONFLICT-style validation | Missing version |
| 409 | CART_VERSION_CONFLICT / caps | Any failure rolls the whole batch back; the version does not advance |
8.6 PUT /api/mobile/cart/items/:variantPublicId
Purpose
The debounce target. Absolute quantity — retrying sets the same number twice, which is the same number. Upsert: setting a quantity on a product not in the cart adds it. quantity: 0 removes the line, including for a product since archived.
Request
{ "quantity": 6, "version": 7 }Response
200 — whole cart.
Error Cases
Same as §8.4 minus the idempotency-related ones (structurally idempotent).
8.7 DELETE /api/mobile/cart/items/:variantPublicId
Purpose
Remove a line. Never a 404, and never gated on the product's lifecycle — removing a variant that is not in the cart, or that names nothing, is a success; telling those apart would let anyone enumerate variant ids. version travels as a query parameter.
A no-op removal does not bump version. Both a product id and a variant id are uuid7, so @IsUUID("7") accepts either — a client still sending the old-style product id resolves to no line, removes nothing, and gets 200 with the cart unchanged, including its version. This closed a gap where the old-style id used to invalidate every other device's optimistic-concurrency token for a change that never happened.
Response
200 — whole cart, version unchanged when nothing was removed.
8.8 DELETE /api/mobile/cart/items
Purpose
Empty the cart — removes every line, keeps the cart itself. Idempotent. version as a query parameter.
Response
200 — whole cart.
8.9 GET /api/admin/carts
Purpose
Read-only list, always paginated — the one list endpoint in the API that refuses pagination=false (sending it is a 400), because cart grows one row per customer plus one per order and the list aggregates over the full join. inactiveForDays + status=active is the abandoned-cart list. A date-only createdTo is inclusive of that whole day.
Auth and Permissions
Cart_READ; ADMIN_READ 30/min.
Response
200 — paginated rows with aggregates. Admin subtotal is computed at live prices over every line, including unbuyable ones.
8.10 GET /api/admin/carts/:cartId
Purpose
Read-only detail. Adds lines with a three-field product reference (id, name, sku) — not the storefront contract — and lastKnownUnitPrice.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 404 | CART_NOT_FOUND | No cart with that id |
9. Flow Diagrams
9.1 Route Ownership
9.2 Request Sequence (set quantity)
9.3 Error Branch (add)
10. Pagination, Sorting, Filtering, and Search
| Endpoint | Pagination Type | Default Size | Max Size | Sort Fields | Filters | Result Cap |
|---|---|---|---|---|---|---|
GET /api/admin/carts | offset, always | 20 | 100 | lastActivityAt (default), createdAt, updatedAt, itemCount | status, customerId, inactiveForDays, createdFrom, createdTo | — |
| Customer cart reads | none | — | — | — | — | 50 lines / 99 per line |
pagination=false is refused with a 400 on the admin list — no such parameter exists there. The customer cart is never paginated.
11. Caching, Jobs, and External Integrations
| Integration | Used? | Details |
|---|---|---|
| Redis cache | Yes | Badge counters only — 30s (CACHE_TTL.VOLATILE), cleared on the customer's own writes, deliberately version-less; the cart itself is never cached |
| 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/cart | get | — | CartQueryService.buildResponse | JWT+IpThrottle | — | — | — | cart, items, products, inventory | — | Yes |
GET /api/mobile/cart/summary | summary | — | …getCounts | JWT+IpThrottle | — | 30s | — | cart, items | — | Yes |
GET /api/mobile/cart/checkout-validation | checkoutValidation | — | …buildCheckoutValidation | JWT+IpThrottle | — | — | — | everything fresh | — | Yes |
POST /api/mobile/cart/items | add | AddCartItemDto | CartCustomerService.add | JWT+IpThrottle+Idempotency | — | clear | — | cart, cart_item, products | 400/404/409 | Yes |
POST /api/mobile/cart/items/bulk | bulk | BulkCartMutationDto | CartBulkService.apply | JWT+IpThrottle | — | clear | — | cart, many cart_item, products | 400/409 | Yes |
PUT /items/:variantPublicId | setQuantity | SetCartItemQuantityDto | CartCustomerService.setQuantity | JWT+IpThrottle | — | clear | — | cart, cart_item | 400/404/409 | Yes |
DELETE /items/:variantPublicId | remove | CartVersionQueryDto | …remove | JWT+IpThrottle | — | clear | — | cart, cart_item | — | Yes |
DELETE /items | clear | CartVersionQueryDto | …clear | JWT+IpThrottle | — | clear | — | cart, cart_item | — | Yes |
GET /api/admin/carts | list | ListCartsQueryDto | CartAdminService.list | JWT+Role+IpThrottle | Cart_READ | — | — | cart ⋈ item ⋈ product | 400 (no pagination param) | Yes |
GET /api/admin/carts/:cartId | findOne | CartAdminParamsDto | CartAdminService.findOne | JWT+Role+IpThrottle | Cart_READ | — | — | cart + lines | 404 | Yes |
13.2 Request/Response Exhaustiveness
Covered in §8: minimal/full request bodies (§6.1–6.3/8.4–8.6), success responses (§8.1, §8.6), empty-cart response (§6.5/8.1), the invalid-line-with-reason shape (§6.5), the two shapes of "too many units" (400 vs 409, §8.4), domain errors per endpoint (§8 error tables), rate-limit behavior (account-keyed; mutation budget 60/min), admin 400-on-pagination (8.9).
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.
13.4 Consumer Integration Notes
| Consumer | Required Knowledge | Failure Handling | Contract Stability |
|---|---|---|---|
| Web frontend | Debounce with absolute PUT; whole-cart responses; version for steppers | 409 CART_VERSION_CONFLICT → refetch and reapply; 404 on add → refresh product | Stable |
| Mobile app | Idempotency-Key if retrying POST /items; account-keyed rate limits | 429 → back off | Stable |
| Admin panel | Always-paginated list; date-only createdTo inclusive; read-only | 400 on pagination param | Stable |
| QA | Exact vs approximate caps, oversell lines valid, price-change acknowledgement | Reproduce via exact codes | Stable |
| Checkout (future) | Must own the checkout_locked return edge; snapshot nothing from the cart for charging | 409 CART_LOCKED_FOR_CHECKOUT semantics | Stable |
13.5 API Tradeoffs and Rationale
| Decision | Chosen Behavior | Alternatives Considered | Why This Tradeoff | Risk | Mitigation |
|---|---|---|---|---|---|
Absolute PUT | Retry-safe stepper | Delta PUT | Coalesced flush cannot double-apply | Client must know the target | Upsert removes the guess |
| One non-idempotent POST | Delta add with optional key | All-delta | Product card works blind | Retry doubles without header | Optional Idempotency-Key |
| Version optional except bulk | Trust model | Always required | Product-card add has no cart | Silent clobber | Documented; bulk enforced |
| Live data on read | Never stale | Snapshot on add | Cart is intent, not a charge | Read cost | Batched seam |
| Admin always paginated | Refuse unbounded | Accept pagination=false | Aggregate list is unbounded | — | Loud 400 |
| Admin read-only | No cart tampering | Admin edits | No audit trail exists | Operators can't fix | Accepted |
| Mutation budget 60/min | Stepper bursts allowed | 20/min | Being rate-limited out of your own cart is an outage | Abuse window | Account-keyed |
13.6 API Change Impact
| Change | Affected Consumers | Backend Impact | Data Impact | Migration Needed? | Compatibility Plan |
|---|---|---|---|---|---|
checkout_locked written (future) | Cart clients | Status guard | None | No | Nothing writes it today; contract frozen |
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.5, §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 minimal request, success response 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/cart/backend
- Features and flows doc: /docs/developer/cart/feature
- TDD: not yet published