← All @molecule/* packages · App templates
@molecule/api-resource-cartAPI resource · cart · API (Node) · v1.0.1 · Apache-2.0
Shopping cart resource with item management, coupon support, and computed totals.
npm install @molecule/api-resource-cart@molecule/api-resource-cart is an API resource: the routes, validation and storage for cart, built on the database and auth cores so it runs on whichever providers your app has bonded.
import { routes, requestHandlerMap } from '@molecule/api-resource-cart'Works with: @molecule/api-i18n, @molecule/api-resource
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Shopping cart resource for molecule.dev.
Provides a user-scoped singleton cart with item management, coupon support, and computed totals (subtotal, discount, tax, total).
import { routes, requestHandlerMap } from '@molecule/api-resource-cart'
resource
npm install @molecule/api-resource-cart @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-resource
AddCartItemInputInput for adding an item to the cart.
interface AddCartItemInput {
/** The product to add. */
productId: string
/** Optional variant identifier. */
variantId?: string
/** Display name of the product. */
name: string
/** Unit price. */
price: number
/** Quantity to add. */
quantity: number
/** Optional product image URL. */
image?: string
/** Arbitrary metadata. */
metadata?: Record<string, unknown>
}
AppliedCouponA coupon applied to a cart.
interface AppliedCoupon {
/** The coupon code. */
code: string
/** The discount type. */
type: 'percentage' | 'fixed'
/** The discount value (percentage 0–100, or fixed amount). */
value: number
}
ApplyCouponInputInput for applying a coupon.
interface ApplyCouponInput {
/** The coupon code to apply. */
code: string
}
CartA shopping cart with items, totals, and optional coupon.
interface Cart {
/** Unique cart identifier. */
id: string
/** Owner of this cart. */
userId: string
/** Items in the cart. */
items: CartItem[]
/** Currently applied coupon, if any. */
coupon?: AppliedCoupon
/** Sum of item prices × quantities before discounts. */
subtotal: number
/** Discount amount from applied coupon. */
discount: number
/** Computed tax amount. */
tax: number
/** Final total: subtotal − discount + tax. */
total: number
/** Last modification timestamp. */
updatedAt: string
}
CartItemA shopping cart item.
interface CartItem {
/** Unique identifier for this cart item entry. */
id: string
/** The product being purchased. */
productId: string
/** Optional variant (size, color, etc.). */
variantId?: string
/** Display name of the product. */
name: string
/** Unit price of the item. */
price: number
/** Quantity in the cart. */
quantity: number
/** Optional product image URL. */
image?: string
/** Arbitrary metadata attached to this item. */
metadata?: Record<string, unknown>
}
CartItemRowInternal database row for a cart item.
interface CartItemRow {
/** Unique cart item identifier. */
id: string
/** Parent cart identifier. */
cartId: string
/** The product being purchased. */
productId: string
/** Optional variant identifier. */
variantId: string | null
/** Display name. */
name: string
/** Unit price. */
price: number
/** Quantity. */
quantity: number
/** Optional product image URL. */
image: string | null
/** JSON-serialized metadata. */
metadata: string | null
/** Creation timestamp. */
createdAt: string
}
CartRowInternal database row for a cart.
interface CartRow {
/** Unique cart identifier. */
id: string
/** Owner of this cart. */
userId: string
/** JSON-serialized coupon data, or null. */
coupon: string | null
/** Creation timestamp. */
createdAt: string
/** Last modification timestamp. */
updatedAt: string
}
CartSummarySummary snapshot of a cart.
interface CartSummary {
/** Total number of items (sum of quantities). */
itemCount: number
/** Number of distinct products. */
uniqueItems: number
/** Subtotal before discounts. */
subtotal: number
/** Final total after discounts and tax. */
total: number
}
UpdateCartItemInputInput for updating an item's quantity.
interface UpdateCartItemInput {
/** New quantity for the item. Must be >= 1. */
quantity: number
}
addItem(req, res)Adds an item to the authenticated user's cart. If the same product+variant already exists, increments the quantity instead of creating a duplicate entry.
⚠️ CLIENT-PRICE TRUST BOUNDARY ⚠️ — the stored price comes verbatim from
the request body. This resource is GENERIC (no product/catalog table), so it
CANNOT verify the price; the validation below only rejects malformed money
(negative price, non-integer/zero quantity). NEVER charge a customer off a
cart-item price: at checkout, re-resolve every unit price SERVER-SIDE from
the product/menu table (keyed by productId/variantId) and recompute
totals from those trusted values, ignoring the client-supplied price.
function addItem(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with {@link AddCartItemInput} body.res — The response object.applyCoupon(req, res)Applies a coupon code to the user's cart. The coupon is validated by looking
it up in the coupons table. Replaces any previously applied coupon.
function applyCoupon(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with {@link ApplyCouponInput} body.res — The response object.assembleCart(cartRow, itemRows)Assembles a full {@link Cart} object from a database cart row and item rows.
function assembleCart(cartRow: CartRow, itemRows: CartItemRow[]): Cart
cartRow — The cart database row.itemRows — The cart item database rows.Returns: The assembled cart.
clearCart(_req, res)Removes all items and the applied coupon from the user's cart.
function clearCart(_req: MoleculeRequest, res: MoleculeResponse): Promise<void>
_req — The request object (unused).res — The response object.computeDiscount(coupon, subtotal)Computes the discount from an applied coupon and a subtotal.
function computeDiscount(coupon: AppliedCoupon | undefined, subtotal: number): number
coupon — The applied coupon, if any.subtotal — The cart subtotal.Returns: The discount amount (clamped to subtotal).
computeSubtotal(items)Computes the subtotal from a list of cart items (price × quantity).
function computeSubtotal(items: CartItem[]): number
items — The cart items.Returns: The subtotal amount.
computeTax(taxableAmount)Computes tax from the taxable amount.
function computeTax(taxableAmount: number): number
taxableAmount — The amount to tax (subtotal − discount).Returns: The tax amount.
getCart(_req, res)Returns the authenticated user's cart. Creates an empty cart if none exists.
function getCart(_req: MoleculeRequest, res: MoleculeResponse): Promise<void>
_req — The request object (unused — cart is identified by session user).res — The response object.getCartSummary(_req, res)Returns a lightweight summary of the user's cart (item count, totals).
function getCartSummary(_req: MoleculeRequest, res: MoleculeResponse): Promise<void>
_req — The request object (unused).res — The response object.removeCoupon(_req, res)Removes the currently applied coupon from the user's cart.
function removeCoupon(_req: MoleculeRequest, res: MoleculeResponse): Promise<void>
_req — The request object (unused).res — The response object.removeItem(req, res)Removes an item from the user's cart by item ID.
function removeItem(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with itemId route parameter.res — The response object.toCartItem(row)Converts a database cart-item row into a typed {@link CartItem}.
function toCartItem(row: CartItemRow): CartItem
row — The raw database row.Returns: The deserialized cart item.
updateQuantity(req, res)Updates the quantity of a specific item in the user's cart.
function updateQuantity(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with itemId param and {@link UpdateCartItemInput} body.res — The response object.i18nRegisteredWhether i18n registration has been attempted. Always true; this module is
a placeholder for symmetry with locale-bonded resources.
const i18nRegistered: true
requestHandlerMapHandler map for the cart resource routes.
const requestHandlerMap: {
readonly getCart: typeof getCart
readonly addItem: typeof addItem
readonly updateQuantity: typeof updateQuantity
readonly removeItem: typeof removeItem
readonly clearCart: typeof clearCart
readonly applyCoupon: typeof applyCoupon
readonly removeCoupon: typeof removeCoupon
readonly getCartSummary: typeof getCartSummary
}
routesShopping cart routes. The cart is a user-scoped singleton resource
(one cart per authenticated user), so routes use /cart (singular).
const routes: readonly [
{
readonly method: 'get'
readonly path: '/cart'
readonly handler: 'getCart'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'post'
readonly path: '/cart/items'
readonly handler: 'addItem'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'put'
readonly path: '/cart/items/:itemId'
readonly handler: 'updateQuantity'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'delete'
readonly path: '/cart/items/:itemId'
readonly handler: 'removeItem'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'delete'
readonly path: '/cart'
readonly handler: 'clearCart'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'post'
readonly path: '/cart/coupon'
readonly handler: 'applyCoupon'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'delete'
readonly path: '/cart/coupon'
readonly handler: 'removeCoupon'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'get'
readonly path: '/cart/summary'
readonly handler: 'getCartSummary'
readonly middlewares: readonly ['authenticate']
},
]
Peer dependencies:
@molecule/api-database ^1.0.1@molecule/api-i18n ^1.0.1@molecule/api-logger ^1.0.1@molecule/api-resource ^1.0.1@molecule/api-database@molecule/api-i18n@molecule/api-logger@molecule/api-resourceSECURITY — cart item prices are CLIENT-SUPPLIED and unverified. This
resource is GENERIC (no product/catalog table), so addItem() stores the
price from the request body verbatim; the computed subtotal/discount/
tax/total (see utilities.ts) are derived from those client prices.
Input validation rejects malformed money (negative price, non-integer or
< 1 quantity) but does NOT establish that the prices are CORRECT — a
client can submit price: 0. NEVER charge a customer off a cart total: at
checkout, re-resolve every unit price SERVER-SIDE from the product/menu table
(keyed by productId/variantId) and recompute the totals from those
trusted values, ignoring the client-supplied price. The cart total is for
DISPLAY only. (applyCoupon() is already safe — it looks the coupon up in
the coupons table and ignores any client-supplied discount value.)
Migration required — and coupons is NOT included.
src/__setup__/carts.sql creates carts + cart_items (scaffolded apps
apply it automatically). applyCoupon() looks coupons up in a coupons
table this package does NOT create — provide one (your own migration or a
commerce package that ships it) before surfacing coupon UI, or the mounted
POST /cart/coupon route fails at runtime.
The cart is a per-user singleton, keyed by the session. GET /cart
creates the row on first read; never accept a cartId/userId from the
client — all routes operate on the caller's own cart.
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
POST /cart/items) puts a line in the cart at the
quantity requested, and the cart view + subtotal reflect it.productId/variantId) again MERGES —
the existing line's quantity increments, it does NOT create a second
duplicate line; a different product stays its own line.PUT /cart/items/:itemId) and removing a
line (DELETE /cart/items/:itemId) update the cart, and the computed
subtotal/total RECOMPUTE correctly: subtotal is the sum of price × quantity,
and total = subtotal - discount + tax (tax is 0 until you wire a rate).
Apply a coupon (POST /cart/coupon, looked up server-side in the coupons
table) and confirm discount + total drop; removing it restores them.DELETE /cart) empties it — items and any applied
coupon gone, totals back to 0. If the app builds checkout, it hands off the
right items and a SERVER-recomputed total, never the client cart total.cartId/userId. A second signed-in user sees only their
own cart, and a line itemId from another user's cart is neither readable
nor mutable (update/remove return not-found) — no path to someone else's cart.