Managing Cart Line Items

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.

// src/lib/repositories/cartsRepo.ts
export async function getCartItemByCartAndId(
  cartId: string,
  itemId: string,
): Promise<CartItem | null> {
  const rows = await query<CartItemRow>(
    "SELECT * FROM cart_items WHERE cart_id = $1 AND id = $2",
    [cartId, itemId],
  );
  return rows[0] ? mapCartItemRow(rows[0]) : 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.

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