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.
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.
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:
Validation logic for the request body lives in:
src/lib/services/cartsService.ts
This ensures bad input never reaches business logic or SQL.
- The function accepts
unknown, forcing validation. This is a key backend discipline: all network input is untrusted until proven safe. isUUID(v?.product_id)ensures the product identifier is syntactically valid before querying the database. Without this, you could trigger confusing errors or unnecessary DB load.isInt(v?.quantity, { min: 1 })enforces that quantity is a positive integer. Zero, negative, fractional, or string quantities are rejected.- The return type is a discriminated union: either
{ ok: true; value: ... }or{ ok: false; message: ... }. This makes route logic straightforward and type-safe. - The validated value is rebuilt explicitly, ensuring only approved fields pass through.
This function protects everything downstream.
Now we move to the core business logic in:
src/lib/services/cartsService.ts
This is where lifecycle rules, product checks, and inventory enforcement happen.
-
The cart is loaded first.
ensureCartOpen(cart)enforces two rules: missing cart →404, non-open cart →409 CONFLICT. This prevents adding items to checked-out or abandoned carts. -
The product must exist. If not found, we return
404. That’s a true missing resource. -
If the product is
"archived", we return409 CONFLICT. The product exists but is not purchasable.
The persistence logic lives in:
src/lib/repositories/cartsRepo.ts
This function must safely handle concurrent “Add to Cart” requests.
-
withTransaction(...)guarantees atomicity of the write you choose to perform (theUPDATEor theINSERT) and ensures the function either fully succeeds or fully rolls back if something fails mid-way. -
However, in standard PostgreSQL READ COMMITTED isolation, wrapping
SELECTthenINSERTin a transaction does not, by itself, prevent race conditions:- Two concurrent requests can both run the
SELECT, both observe “no row exists”, and then both attempt the .
- Two concurrent requests can both run the
Once a line item is added, fetching the cart via GET /api/carts/:id will automatically reflect updated totals because:
subtotal_centsis computed as:sum(quantity * unit_price_cents)tax_centsis computed using:computeTaxCents(subtotal_cents)total_centsis:subtotal_cents + tax_cents
Since totals are derived in the repository during reads, no extra write is needed when adding items. This keeps totals consistent and avoids storing redundant computed data.
In this lesson, you implemented a production-grade Add to Cart flow:
-
The route validates
:id, enforcesPOST, safely parses JSON, validates input, and forwards service errors using service-providedhttpStatus. -
validateAddItemensuresproduct_idis a UUID andquantityis an integer ≥ 1. -
addItemServiceenforces lifecycle and inventory rules:- Cart must exist and be open (404 / 409).
- Product must exist (404).
- Product must not be archived (409).
- Inventory must not be exceeded, including existing cart quantity.
-
addOrIncrementCartItemruns inside a transaction to prevent duplicate rows and ensure atomic increments under concurrency.
With this in place, your cart system now supports safe, correct, and scalable item additions—just like a real e-commerce backend.
