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_id that was added.
  • The quantity requested.
  • The unit_price_cents at 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 :id URL 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 Created on success.

Let’s walk through it carefully.

First, the imports and route function:

Service-Level Validation: validateAddItem

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.

Service: addItemService Business Rules

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 return 409 CONFLICT. The product exists but is not purchasable.

Repository: addOrIncrementCartItem, Concurrency, and Correctness

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 (the UPDATE or the INSERT) and ensures the function either fully succeeds or fully rolls back if something fails mid-way.

  • However, in standard PostgreSQL READ COMMITTED isolation, wrapping SELECT then INSERT in 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 .
How Totals Automatically Reflect New Items

Once a line item is added, fetching the cart via GET /api/carts/:id will automatically reflect updated totals because:

  • subtotal_cents is computed as: sum(quantity * unit_price_cents)
  • tax_cents is computed using: computeTaxCents(subtotal_cents)
  • total_cents is: 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.

Recap

In this lesson, you implemented a production-grade Add to Cart flow:

  • The route validates :id, enforces POST, safely parses JSON, validates input, and forwards service errors using service-provided httpStatus.

  • validateAddItem ensures product_id is a UUID and quantity is an integer ≥ 1.

  • addItemService enforces 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.
  • addOrIncrementCartItem runs 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.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal