Clicks & Carts

Building a cart with the Shopify Storefront API

The Cart API is what headless storefronts and custom carts are built on. The mechanics are simple; persistence and checkout handoff are where it goes wrong.

8 min read · APIs & data ·

The Storefront API's Cart object is what you build on when the storefront isn't a Liquid theme — a headless build, a mobile app, or a fast custom cart inside an otherwise conventional theme. The mechanics are straightforward. Persistence and the handoff to checkout are where implementations go wrong.

The shape of it

A cart is created, mutated, and eventually handed to Shopify's checkout. Four mutations do almost everything:

``graphql mutation { cartCreate(input: { lines: [{ merchandiseId: "gid://shopify/ProductVariant/123", quantity: 1 }] }) { cart { id checkoutUrl totalQuantity } userErrors { field message } } } ``

Then cartLinesAdd, cartLinesUpdate and cartLinesRemove for changes. Every one returns userErrors — select and check it, because a mutation can succeed at the HTTP level and fail at the business level.

merchandiseId is a variant ID, not a product ID. Products aren't purchasable; variants are.

Cart ID persistence is the whole problem

The cart lives on Shopify. You hold its ID. If you lose the ID, the customer loses their cart.

Practically:

  • Store the cart ID in a cookie or local storage, with a sensible expiry.
  • Handle a stale ID. Carts expire. If a mutation fails because the cart no longer exists, create a fresh one rather than showing an error — this is the single most common headless cart bug in production.
  • Re-fetch on load rather than trusting cached contents. Prices, availability and discounts change.
  • Decide what happens on login. Merging a guest cart with a returning customer's is a deliberate decision, not a default.

Line item attributes

Custom data per line — engraving text, delivery date, a gift message, a configurator's output:

``graphql lines: [{ merchandiseId: "gid://shopify/ProductVariant/123", quantity: 1, attributes: [{ key: "Engraving", value: "For Anna" }] }] ``

These flow through to the order, which is what makes them useful — the warehouse sees them. Cart-level attributes work the same way for order-wide information.

Buyer identity

``graphql cartBuyerIdentityUpdate(cartId: $id, buyerIdentity: { countryCode: DE, customerAccessToken: $token }) ``

Setting the country gets you correct pricing and available payment methods for that market, which matters in a multi-market store. Setting the customer access token means checkout is pre-filled and the order is attributed to their account — the token comes from the Customer Account API.

Discounts

``graphql cartDiscountCodesUpdate(cartId: $id, discountCodes: ["SUMMER10"]) ``

The response tells you whether each code was applicable. Check it — an invalid code returned without an error message produces a cart showing no discount and a customer who thinks your site is broken.

Read cost rather than computing totals yourself: subtotalAmount, totalAmount, totalTaxAmount, totalDutyAmount. Any total you calculate in the front end will eventually disagree with checkout.

The handoff to checkout

A cart carries a checkoutUrl. Redirect to it and Shopify's checkout takes over.

This is a good thing and worth understanding: you don't rebuild checkout. Shopify's is heavily optimised, handles payments, fraud, taxes and compliance, and converts better than anything you'd build. Your job ends at the handoff.

What you can still influence is checkout behaviour through extensions and Functions, on plans that support them.

Performance

Carts are read on almost every page. Two habits:

  • Request only the fields you render. Cart queries get large quickly and you pay for depth.
  • Update optimistically in the UI, then reconcile with the server response. A cart that waits 400ms before showing an added item feels broken.

When you don't need this

If you're building a Liquid theme, the theme's own cart already works and Shopify's AJAX cart endpoints handle updates without touching the Storefront API. Reaching for the Cart API inside a normal theme adds complexity for little gain.

The genuine cases are: a headless storefront, a mobile app, or one specific high-value interaction — a configurator, a quick-add drawer — inside an otherwise Liquid theme. That last pattern is how most stores actually use the Storefront API, and it's covered in Storefront API vs Liquid.

The Cart API is easy. Losing the cart ID, showing a total you calculated yourself, and silently ignoring a rejected discount code are the three bugs that ship anyway.

Is this the problem you’re looking at?

Send me the link to your store and a line about what is going wrong. You get a straight answer within one business day — no pitch, no obligation.

[email protected]

Or see what I do around Shopify: services, work beyond the theme, selected work.

Keep reading

← All articles