Managing Cart Line Items

Welcome back! 👋 Now that your cart can gain items via “add to cart,” the next step is letting clients manage those items: change quantities and remove a line entirely.

In this lesson, you’ll implement the two core mutations that every cart needs: PATCH to update a line item’s quantity, and DELETE to remove a line item. Along the way, you’ll see how the repository provides precise “row-level” operations, how the service layer enforces business rules (cart state, product availability, inventory), and how the route maps those outcomes into consistent HTTP responses without hardcoded status codes.

Previously, adding items created rows in cart_items, and cart reads automatically computed totals from those rows. Managing items builds directly on that: when quantities change or rows are deleted, the cart totals naturally change on the next GET /api/carts/:id.

The Cart Item Operations We’re Adding

A cart line item is uniquely identified by:

  • cart_id (the cart that owns it)
  • id (the item row’s ID)

That means every “manage” operation needs to be scoped by both values. Scoping by both is not just correctness—it’s security and integrity: you never want to update or delete an item from the wrong cart.

In this lesson, you’ll cover three repository capabilities that make safe mutations possible:

  • getCartItemByCartAndId(cartId, itemId) → read a specific item (or null)
  • updateCartItemQuantity(cartId, itemId, quantity) → update quantity with UPDATE ... RETURNING (or null)
  • deleteCartItem(cartId, itemId) → delete with DELETE ... RETURNING (returns boolean)

Then you’ll wire those into service rules and expose them through:

app/routes/api.carts.$id.items.$itemId.ts

Repository: Reading a Specific Cart Item Before Mutating

The service layer often needs to detect “missing item” before it tries to update or delete. That’s why the repository provides a targeted lookup.

This code lives in src/lib/repositories/cartsRepo.ts and returns either the cart item or null.

  • The key detail is the WHERE cart_id = $1 AND id = $2 scope. This guarantees you only ever retrieve an item that belongs to the specified cart, which prevents cross-cart access bugs.
  • Returning null is intentional: the repository is “SQL-in / data-out.” It doesn’t decide HTTP behavior; it gives the service enough information to turn missing rows into a 404.
  • mapCartItemRow keeps the returned shape consistent with the rest of the domain layer, so services and routes don’t work with raw DB rows.

This function is especially valuable because it lets the service check business rules (like product availability) using the item’s product_id before applying updates.

Repository: Updating Quantity with UPDATE ... RETURNING

When updating quantity, we want a single database statement that both performs the update and returns the updated row. That’s what RETURNING * gives us.

This function lives in src/lib/repositories/cartsRepo.ts.

  • The update is scoped by both cart_id and id. This prevents accidental updates to an item row that happens to exist but belongs to a different cart.
  • updated_at = now() ensures the timestamp reflects the server-side mutation time. Clients don’t control timestamps, and you want consistent auditability.
  • RETURNING * removes the need for a follow-up SELECT. You update and retrieve the new state in one round trip, which is both simpler and more efficient.
  • Returning null when no row is returned provides a clean “not found” signal. The service can convert that into a 404 without trying to infer from affected row counts manually.

This repository function is intentionally “dumb” about inventory and product status—those are business rules and belong in the service layer.

Repository: Deleting an Item and Returning a Boolean

For deletes, we want a clear signal: “did we actually delete something?” The simplest way here is DELETE ... RETURNING * and then return true when a row existed.

This function lives in src/lib/repositories/cartsRepo.ts.

  • Again, scoping by (cart_id, id) is essential. It ensures the delete cannot remove a row outside the cart being mutated.
  • Using RETURNING * lets you detect whether a row was actually deleted. If nothing matched, Postgres returns no rows, and Boolean(rows[0]) becomes false.
  • Returning a boolean is a deliberate repository-level contract: the repository answers the persistence question (“did a row exist and get deleted?”), and the service decides what that means (usually 404 if false).

This sets up a clean service flow: check cart open → check item exists → attempt delete → confirm delete.

Service Validation: validateUpdateItem

Before you update anything, the request body must be validated. For this endpoint, the only allowed update is quantity, and the shape must effectively be:

Validation lives in src/lib/services/cartsService.ts. It returns a typed value on success and a clear message on failure.

  • Accepting input: unknown forces the function to treat the body as untrusted, which is the right stance for API code.
  • The validator requires quantity to be an integer and at least 1. This prevents invalid states like zero-quantity rows (which would behave like “soft delete” but without actually deleting).
  • The return shape is a clear union: { ok: false, message } makes route error mapping straightforward and consistent.
  • The output value is reconstructed as { quantity: v.quantity! }, ensuring the rest of the system works only with validated data instead of the raw input object.

In practice, this validator keeps the route simple: parse JSON → validate → call service.

Service: Updating an Item with Business Rules

The service method updateItemService(cartId, itemId, input) enforces the rules that make the update meaningful and safe:

  • Cart must exist and be open (404 / 409)
  • Cart item must exist for that cart (404)
  • Product still exists and is purchasable (missing/archived → 409)
  • Inventory must be sufficient (409)

This code lives in src/lib/services/cartsService.ts.

  • The call to ensureCartOpen is the gatekeeper for cart lifecycle. A checked-out or abandoned cart is a real cart, but it’s not valid to mutate—so the service returns 409 CONFLICT.
  • getCartItemByCartAndId(cartId, itemId) ensures the item exists and belongs to the cart. If it’s missing, we return 404, which is the clean “you can’t update what doesn’t exist” outcome.
Service: Deleting an Item Safely

Deleting also needs lifecycle enforcement and “missing item” checks. The service returns { deleted: true } on success, which makes the API response predictable.

This code lives in src/lib/services/cartsService.ts.

  • The same cart lifecycle gate (ensureCartOpen) applies here. If the cart is not open, deleting items would break the “checked out cart is immutable” rule, so 409 is returned.
  • The explicit “does the item exist?” check makes error messages consistent and intentional. It also lets you fail fast before doing a delete operation.
  • The repository delete returns a boolean, so the service can cleanly map false to 404. This is especially useful for handling concurrency (e.g., two delete requests in a row).
  • Returning { deleted: true } provides a stable response contract: clients don’t need to interpret an empty response body to know what happened.
Route: PATCH and DELETE /api/carts/:id/items/:itemId

The mutation endpoint for managing line items is:

app/routes/api.carts.$id.items.$itemId.ts

This route validates both IDs, supports PATCH and DELETE, and returns consistent envelopes for every outcome.

Here’s the full route:

  • The route validates both params.id and params.itemId with . If either is invalid, it returns immediately. This prevents ambiguous errors and avoids useless DB queries.

What Happens After These Mutations

Once PATCH or DELETE succeeds, the cart totals will automatically reflect the change on the next read:

  • Updating quantity changes subtotal_cents because it changes quantity * unit_price_cents.
  • Deleting an item removes a term from the subtotal sum entirely.
  • Tax and total are recomputed from the new subtotal whenever the cart is fetched.

This is one of the main benefits of computing totals at read time: you avoid storing redundant totals that could become inconsistent.

Recap

You now have full control over cart line items:

  • The repository provides precise, cart-scoped operations (get, update, delete) that return null/boolean signals so services can handle “missing item” cleanly.

  • validateUpdateItem ensures the only acceptable update is { "quantity": <int>=1 }, producing clear validation messages.

  • The service layer enforces business rules for updates and deletes:

    • cart exists + open (404 / 409)
    • item exists (404)
    • product available (409)
    • inventory enforced for updates (409)
  • The route validates both UUID params, implements PATCH and DELETE flows, and maps failures using the service-provided httpStatus for consistent, professional API behavior.

With these endpoints in place, your cart API behaves like a real system: clients can add items, adjust quantities safely, remove items, and always rely on predictable responses and correct totals.

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