Adding Cart Line Items
Adding Cart Line Items
Welcome back! 👋 Now that carts can be created and fetched in a fully hydrated shape (with items and computed totals), it’s time to make them actually useful by adding line items.
In this lesson, you’ll implement the full “Add to Cart” flow: the route validates input and delegates to the service, the service enforces business rules like inventory and cart lifecycle, and the repository performs an atomic add-or-increment write using a transaction. This mirrors how real e-commerce backends handle users clicking “Add to cart” multiple times—correctly and safely, even under concurrency.
Previously, your cart reads were already computing subtotal_cents, tax_cents, and total_cents. Once line items are added, those totals automatically become meaningful—because the repository recomputes them on every read.
What a Cart Line Item Represents
A cart line item corresponds to one row in the cart_items table. It captures:
- The
product_idthat was added. - The
quantityrequested. - The
unit_price_centsat the time of addition (a snapshot).
That unit_price_cents field is critical. Even if the product’s price changes later, your cart totals are computed from what was recorded in the cart. This keeps pricing stable and predictable for the user.
Route: POST /api/carts/:id/items
The entry point for adding items is implemented in:
app/routes/api.carts.$id.items.ts
This route is responsible for:
- Validating the
:idURL parameter. - Allowing only
POST. - Safely parsing JSON with
parseJson(...). - Validating the body using
validateAddItem(...). - Delegating to
addItemService(...). - Mapping service failures using the service-provided
httpStatus. - Returning
201 Createdon success.
Let’s walk through it carefully.
First, the imports and route function:
- The route extracts
params.idand immediately validates it usingisUUID. If the ID is malformed, it returns a400 VALIDATION_ERROR. This prevents meaningless database calls and clearly communicates “your URL is wrong.” - Only
POSTis allowed. Any other method returns405. This keeps the endpoint strict and predictable. parseJson(request)is used instead of directly callingrequest.json(). This ensures malformed JSON doesn’t crash the route and instead becomes a structured400error.validateAddItem(...)converts untrusted input into a strongly shapedAddCartItemInput. If validation fails, the route returns400before hitting any business logic.addItemService(...)returns aServiceResult. The route does not guess HTTP codes—it forwardsresult.error.httpStatus, centralizing domain-to-HTTP mapping inside the service.- On success,
success(result.value, 201)returns201 Created. Even if the row was incremented instead of inserted, it is still a successful mutation.
This route stays thin. It owns HTTP mechanics, not business rules.
