Skip to main content
The shopping cart provides a seamless experience for customers to manage items before checkout, with support for both authenticated users and guest customers.

Cart Architecture

Carts are persisted on the server and referenced by a cookie:
// Cart storage
const cartId = cookies().get('_openfront_cart_id')?.value

// Retrieve cart from backend
const cart = await openfrontClient.request(
  gql`query GetCart($cartId: ID!) {
    activeCart(cartId: $cartId)
  }`,
  { cartId }
)

Cart Creation

Carts are automatically created when a customer adds their first item:
// features/storefront/lib/data/cart.ts
export async function getOrSetCart(countryCode: string) {
  let cartId = await getCartId()
  let cart = null
  
  // Try to retrieve existing cart
  if (cartId) {
    try {
      const result = await openfrontClient.request(CART_QUERY, { cartId })
      cart = result?.activeCart
      
      // If cart doesn't exist, remove invalid cookie
      if (!cart) {
        await removeCartId()
        cartId = undefined
      }
    } catch (error) {
      await removeCartId()
      cartId = undefined
    }
  }
  
  // Get region for country
  const { regions } = await openfrontClient.request(
    gql`query GetRegion($code: String!) {
      regions(where: { countries: { some: { iso2: { equals: $code } } } }) {
        id
      }
    }`,
    { code: countryCode }
  )
  
  // Create new cart if needed
  if (!cart) {
    const { createCart: newCart } = await openfrontClient.request(
      gql`mutation CreateCart($data: CartCreateInput!) {
        createCart(data: $data) {
          id
          region { id }
        }
      }`,
      { data: { region: { connect: { id: regions[0].id } } } }
    )
    
    cart = newCart
    await setCartId(cart.id)
  }
  
  return cart
}

Adding Items

Customers add products to cart by selecting a variant:
// features/storefront/lib/data/cart.ts
export async function addToCart({ 
  variantId, 
  quantity, 
  countryCode 
}) {
  if (!variantId) {
    throw new Error('Missing variant ID when adding to cart')
  }
  
  // Get or create cart
  const cart = await getOrSetCart(countryCode)
  
  if (!cart) {
    throw new Error('Error retrieving or creating cart')
  }
  
  // Add line item to cart
  await openfrontClient.request(
    gql`mutation UpdateActiveCart($cartId: ID!, $data: CartUpdateInput!) {
      updateActiveCart(cartId: $cartId, data: $data) {
        id
        lineItems {
          id
          quantity
        }
      }
    }`,
    {
      cartId: cart.id,
      data: {
        lineItems: {
          create: [{
            productVariant: { connect: { id: variantId } },
            quantity
          }]
        }
      }
    }
  )
  
  revalidateTag('cart')
}

Product Actions Component

The add to cart button on product pages:
// features/storefront/modules/products/components/product-actions/
export default function ProductActions({ product, region }) {
  const [isAdding, setIsAdding] = useState(false)
  const params = useParams()
  const countryCode = params?.countryCode as string
  
  const handleAddToCart = async () => {
    if (!variant?.id) return null
    
    setIsAdding(true)
    await addToCart({
      variantId: variant.id,
      quantity: 1,
      countryCode
    })
    setIsAdding(false)
  }
  
  return (
    <Button 
      onClick={handleAddToCart}
      disabled={isAdding || !inStock || !variant}
    >
      {isAdding && <Loader className="animate-spin" />}
      {!variant ? 'Select variant' : !inStock ? 'Out of stock' : 'Add to cart'}
    </Button>
  )
}

Cart Page

The cart page (/[countryCode]/cart) displays all items and totals:
// features/storefront/modules/cart/templates/index.tsx
const CartTemplate = ({ cart, user }) => {
  return (
    <div className="py-12">
      <div className="max-w-[1440px] mx-auto px-6">
        {cart?.lineItems?.length ? (
          <div className="grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-x-12">
            {/* Left: Cart items */}
            <div className="flex flex-col gap-y-6">
              {!user && (
                <>
                  <SignInPrompt />
                  <Divider />
                </>
              )}
              <ItemsTemplate items={cart.lineItems} region={cart.region} />
            </div>
            
            {/* Right: Summary */}
            <div className="sticky top-12">
              <Summary cart={cart} />
            </div>
          </div>
        ) : (
          <EmptyCartMessage />
        )}
      </div>
    </div>
  )
}

Line Items

Each cart item (line item) displays product details and quantity controls:
// features/storefront/modules/cart/components/item/
const CartItem = ({ item, region }) => {
  const { productVariant, quantity } = item
  
  return (
    <div className="flex gap-4">
      {/* Product image */}
      <div className="w-24 h-24 relative">
        <Image
          src={productVariant.product.thumbnail}
          alt={productVariant.product.title}
          fill
          className="object-cover"
        />
      </div>
      
      {/* Product details */}
      <div className="flex-1">
        <h3>{productVariant.product.title}</h3>
        <p className="text-muted-foreground">
          {productVariant.title}
        </p>
        
        {/* Quantity selector */}
        <QuantitySelect
          quantity={quantity}
          onUpdate={(newQuantity) => 
            updateLineItem({ lineId: item.id, quantity: newQuantity })
          }
        />
      </div>
      
      {/* Price */}
      <div className="text-right">
        <p className="font-semibold">
          {formatAmount(item.total, region.currency.code)}
        </p>
        <button onClick={() => deleteLineItem(item.id)}>
          Remove
        </button>
      </div>
    </div>
  )
}

Cart Operations

Update Line Item Quantity

export async function updateLineItem({ lineId, quantity }) {
  const cartId = cookies().get('_openfront_cart_id')?.value
  if (!cartId) return 'No cartId cookie found'
  
  await openfrontClient.request(
    gql`mutation UpdateLineItem(
      $cartId: ID!, 
      $lineId: ID!, 
      $quantity: Int!
    ) {
      updateActiveCartLineItem(
        cartId: $cartId
        lineId: $lineId
        quantity: $quantity
      ) {
        id
        lineItems {
          id
          quantity
          total
        }
      }
    }`,
    { cartId, lineId, quantity }
  )
  
  revalidateTag('cart')
}

Remove Line Item

export async function deleteLineItem(lineId: string) {
  const cartId = cookies().get('_openfront_cart_id')?.value
  if (!cartId) return 'No cart ID found'
  
  await openfrontClient.request(
    gql`mutation UpdateActiveCart($cartId: ID!, $data: CartUpdateInput!) {
      updateActiveCart(cartId: $cartId, data: $data) {
        id
        lineItems { id }
      }
    }`,
    {
      cartId,
      data: {
        lineItems: {
          disconnect: [{ id: lineId }]
        }
      }
    }
  )
  
  revalidateTag('cart')
}

Cart Summary

The cart summary displays totals and discount codes:
const Summary = ({ cart }) => {
  return (
    <div className="bg-background p-6 border rounded-md">
      <h2 className="text-2xl font-semibold mb-4">Summary</h2>
      
      {/* Subtotal */}
      <div className="flex justify-between mb-2">
        <span>Subtotal</span>
        <span>{formatAmount(cart.subtotal, cart.region.currency.code)}</span>
      </div>
      
      {/* Shipping */}
      <div className="flex justify-between mb-2">
        <span>Shipping</span>
        <span>{cart.shippingTotal > 0 
          ? formatAmount(cart.shippingTotal, cart.region.currency.code)
          : 'Calculated at checkout'
        }</span>
      </div>
      
      {/* Tax */}
      <div className="flex justify-between mb-4">
        <span>Tax</span>
        <span>{formatAmount(cart.taxTotal, cart.region.currency.code)}</span>
      </div>
      
      <Divider />
      
      {/* Total */}
      <div className="flex justify-between text-xl font-bold mt-4">
        <span>Total</span>
        <span>{formatAmount(cart.total, cart.region.currency.code)}</span>
      </div>
      
      {/* Checkout button */}
      <Button 
        className="w-full mt-6"
        onClick={() => router.push(`/${countryCode}/checkout`)}
      >
        Checkout
      </Button>
    </div>
  )
}

Discount Codes

Customers can apply discount codes to their cart:
export async function submitDiscountForm(prevState: any, formData: FormData) {
  const cartId = cookies().get('_openfront_cart_id')?.value
  const code = formData.get('code') as string
  
  if (!code) return 'Code is required'
  if (!cartId) return 'No cart found'
  
  try {
    await addDiscountToCart(cartId, code)
    revalidateTag('cart')
    return null
  } catch (error: any) {
    // Extract error message from GraphQL response
    if (error?.response?.errors?.[0]?.extensions?.originalError?.message) {
      return error.response.errors[0].extensions.originalError.message
    }
    return 'Failed to apply discount code'
  }
}

Discount Display

const DiscountCode = ({ cart }) => {
  const [state, formAction] = useFormState(submitDiscountForm, null)
  
  return (
    <form action={formAction}>
      <div className="flex gap-2">
        <Input 
          name="code" 
          placeholder="Discount code"
          className="flex-1"
        />
        <Button type="submit">Apply</Button>
      </div>
      {state && <p className="text-sm text-red-500 mt-2">{state}</p>}
      
      {/* Applied discounts */}
      {cart.discounts?.map(discount => (
        <div key={discount.id} className="flex justify-between mt-2">
          <span className="text-sm">{discount.code}</span>
          <button onClick={() => removeDiscount(discount.code)}>
            Remove
          </button>
        </div>
      ))}
    </form>
  )
}

Gift Cards

Gift cards can be applied to reduce the cart total:
export async function submitGiftCard(code: string) {
  const cartId = await getCartId()
  if (!cartId) return 'No cartId cookie found'
  
  await openfrontClient.request(
    gql`mutation UpdateActiveCart($cartId: ID!, $data: CartUpdateInput!) {
      updateActiveCart(cartId: $cartId, data: $data) {
        id
        giftCards {
          id
          code
          balance
        }
      }
    }`,
    {
      cartId,
      data: {
        giftCards: {
          connect: [{ code }]
        }
      }
    }
  )
  
  revalidateTag('cart')
}

Guest Checkout

Guest customers can shop without creating an account:
const SignInPrompt = () => (
  <div className="bg-muted/40 p-4 rounded-md">
    <p className="text-sm font-medium mb-2">
      Already have an account?
    </p>
    <p className="text-sm text-muted-foreground mb-4">
      Sign in for a better experience
    </p>
    <Button variant="outline" onClick={() => router.push('/account')}>
      Sign in
    </Button>
  </div>
)

Regional Changes

When customers change regions, the cart is updated:
export async function updateRegion(countryCode: string, currentPath: string) {
  const cartId = cookies().get('_openfront_cart_id')?.value
  
  // Revalidate data
  revalidateTag('regions')
  revalidateTag('products')
  
  // Update cart region if cart exists
  if (cartId) {
    const { regions } = await openfrontClient.request(
      gql`query GetRegion($code: String!) {
        regions(where: { countries: { some: { iso2: { equals: $code } } } }) {
          id
        }
      }`,
      { code: countryCode }
    )
    
    await openfrontClient.request(
      gql`mutation UpdateActiveCart($cartId: ID!, $data: CartUpdateInput!) {
        updateActiveCart(cartId: $cartId, data: $data) {
          id
        }
      }`,
      {
        cartId,
        data: { region: { connect: { id: regions[0].id } } }
      }
    )
    
    revalidateTag('cart')
  }
  
  redirect(`/${countryCode}${currentPath}`)
}

Empty Cart

When the cart is empty, show a helpful message:
const EmptyCartMessage = () => (
  <div className="text-center py-12">
    <ShoppingCart className="mx-auto h-12 w-12 text-muted-foreground mb-4" />
    <h2 className="text-2xl font-semibold mb-2">Your cart is empty</h2>
    <p className="text-muted-foreground mb-6">
      Looks like you haven't added anything to your cart yet
    </p>
    <Button onClick={() => router.push(`/${countryCode}`)}>
      Start shopping
    </Button>
  </div>
)

Next Steps

Checkout

Learn about the multi-step checkout process

Cart API

GraphQL mutations for cart management

Build docs developers (and LLMs) love