Skip to main content
Shopify Web DesignCustom Shopify ThemesHeadless Shopify HydrogenShopify UI/UX DesignFigma to ShopifyShopify CROShopify App IntegrationShopify Store Speed SEOShopify Liquid DevelopmentShopify Oxygen HostingBespoke Shopify Design

How I Build a Next.js Shopping Cart on a Laravel API

Published: September 9, 2026
Written by Vindh Sharma
How I Build a Next.js Shopping Cart on a Laravel API
TL;DR

Building a Next.js shopping cart on a Laravel API means the storefront never owns cart logic directly — every add, update, and removal is a request to the Laravel API, and the Next.js frontend simply renders whatever the API's response says the cart contains. I structure it this way so the cart drawer, the cart page, and every product page all show exactly the same state, because they're all reading from the same source rather than three separate local calculations that can drift apart.

Building a Next.js shopping cart on a Laravel API means the storefront never owns cart logic directly — every add, update, and removal is a request to the Laravel API, and the Next.js frontend simply renders whatever the API's response says the cart contains. I structure it this way so the cart drawer, the cart page, and every product page all show exactly the same state, because they're all reading from the same source rather than three separate local calculations that can drift apart.

Here's exactly how I build this flow — from the storefront's request pattern through to shared cart state, secure customer identity, and the handoff into checkout.

The Storefront-to-API Request Pattern

The storefront-to-API request pattern means every cart action in the Next.js frontend — add to cart, change quantity, remove an item — sends a request to a specific Laravel endpoint and waits for that endpoint's response before updating anything on screen. I never let the frontend guess at what the new cart state should look like and update the UI optimistically before the API confirms it, because that guess can be wrong — a stock check might fail, a price might have changed — and a UI that briefly shows an incorrect state is worse than a UI that waits a moment for the real answer.

Each of these requests is a plain HTTP call from Next.js to the Laravel API, authenticated the same way regardless of which page in the storefront triggered it. This keeps the request pattern consistent whether a customer adds an item from a product page, a collection page, or a quick-add button in a related-products carousel.

I keep this request layer thin and consistent rather than letting individual components build their own request logic. A single, shared client function handles the actual call to the Laravel API, and every component that needs to mutate the cart — a product page's add-to-cart button, a cart line's quantity stepper, a collection page's quick-add — calls that same function rather than each writing its own fetch logic with slightly different error handling. This consistency is what makes the frontend easy to maintain: a change to how errors are surfaced, or how a loading state is shown, only needs to happen in one place.

Shared Cart State Across the Storefront

Shared cart state across the storefront means the cart drawer, the dedicated cart page, and the small cart-count badge in the header are all reading from one client-side cart context, and that context itself holds no logic beyond storing whatever the Laravel API's most recent response contained. When any component triggers a cart mutation, the context updates from the API's response, and every other component subscribed to that context re-renders automatically with the new state.

This is what prevents the specific bug I see most often in home-grown headless carts: the header badge shows one quantity, the cart drawer shows another, because each was updating its own local copy independently instead of reading from a single shared source. One context, one source of truth, no drift.

For products with configurable options — size, colour, and similar variants — the same shared-context principle extends down to how a specific configuration is represented in the cart. Each configuration is its own line in the shared state, matching the exact configuration and fingerprint logic I use in any Laravel cart, so a medium-blue and a large-blue addition are tracked as two distinct lines even though they're variations of the same product.

Diagram showing a cart drawer, cart page, and header badge all subscribing to one shared cart context fed by a single API responseEvery cart-related component reads from one shared context — never its own separate local copy.

The Cart Drawer and the Cart Page

The cart drawer gives a customer a quick, non-disruptive way to review what they've added without leaving the page they're on, while the dedicated cart page gives more room for a fuller review — shipping estimates, promotional code entry, and a clearer layout for a cart with many items. I build both against the exact same underlying cart data, so a change made in the drawer is instantly reflected on the cart page and vice versa.

Deciding when to show the drawer versus linking to the full cart page is a UX decision as much as a technical one — I generally open the drawer automatically right after an add-to-cart action, so the customer gets immediate confirmation, but I keep the full cart page as the destination once someone is ready to review everything before checkout.

Both surfaces also have to handle the same edge cases gracefully: a line that's become invalid because a configuration sold out, a price that changed since the item was added, or a quantity that needs capping because stock has dropped. I build these states once, at the shared-context level, so the drawer and the cart page render the same warning or adjustment consistently rather than each needing its own logic for the same situation.

Secure Customer Identity with Sanctum

Secure customer identity in this architecture runs through Laravel Sanctum, which issues a token the Next.js frontend stores and attaches to every authenticated cart request. Once a customer logs in, their cart becomes tied to their account rather than a temporary browser session, and every request the frontend makes on their behalf carries that token so the API knows exactly whose cart it's mutating.

For guests, I use a separate signed token that identifies an anonymous session without requiring an account — this guest cart persists the same way an authenticated cart does, and if the guest later logs in or registers, I merge the guest cart into their new authenticated cart rather than losing it. I've covered this merge behaviour and the broader authentication pattern in more depth in how I connect a Next.js storefront to a Laravel commerce API, since it's central to the architecture as a whole, not just the cart specifically.

Cart Mutations and Server-Authoritative Responses

Cart mutations always return the full, current, server-calculated cart state in the API response — not just a success flag. Adding an item returns the complete updated cart: every line, current prices, current stock status, and the recalculated total, so the Next.js frontend never has to separately fetch the cart again just to know what it now looks like after the mutation.

This matters because it keeps pricing and stock genuinely authoritative at every step, not just at checkout. If a price changed or a configuration sold out between page loads, the very next mutation response reflects that immediately — the customer sees the real, current state of their cart, not a stale one the frontend happened to be holding onto.

This also simplifies the frontend considerably. Because every mutation response is self-contained and complete, the Next.js code never has to reconcile a partial update against what it already had in memory, or make a second follow-up request just to refresh the total. One request, one complete response, one state update — that simplicity is worth more in practice than it sounds, because it removes an entire category of subtle bugs around partial or out-of-order state updates that headless carts are otherwise prone to.

Diagram showing a cart mutation request returning a full, current cart state rather than just a success confirmationEvery mutation returns the complete, current cart — not just a success flag the frontend has to interpret.

Handing Off to Checkout

Handing off to checkout means the cart's final, server-verified state is what checkout begins from — the Next.js frontend passes control to a checkout flow that immediately re-validates everything against the Laravel API rather than trusting whatever the cart displayed a moment ago. Address capture, shipping calculation, and Razorpay payment all happen against this freshly re-verified state, closing any gap between "what the cart showed" and "what actually gets charged."

I treat this handoff as a deliberate step rather than an implicit one — the checkout flow explicitly requests a fresh cart validation as its first action, rather than assuming the cart state it was handed is still accurate by the time a customer reaches the payment step. If a customer opens checkout in one browser tab while adding items to the same cart in another, this fresh validation is what catches the discrepancy before payment, rather than after.

Frequently Asked Questions

Why does the cart wait for the API instead of updating instantly?

Because an instant, optimistic update can be wrong — a stock check might fail or a price might have changed. Waiting a moment for the API's real response means the customer always sees an accurate cart, not a guess that occasionally has to be corrected.

How do the cart drawer and cart page stay in sync?

Both read from the same shared cart context, which itself only holds whatever the Laravel API's most recent response contained. A change in one is immediately visible in the other because there's only one underlying source of state.

What happens to a guest's cart if they create an account partway through shopping?

Their guest cart, identified by a signed anonymous token, is merged into their new authenticated cart rather than discarded. Nothing they've already added is lost.

Does every cart action require a full page reload?

No. Cart mutations are plain API requests handled within the Next.js frontend, so the customer stays on the same page while the cart context updates in the background.

Is pricing rechecked again at checkout, or just when items are added?

Both. Pricing and stock are checked on every cart mutation and re-verified again independently the moment checkout begins, closing any gap that might have opened between browsing and paying.

If you're planning a Next.js storefront and want the cart built on a solid, server-authoritative Laravel foundation from day one, I can help you scope the architecture.

See how I build Next.js carts on a Laravel API

Ready to talk through your storefront and cart requirements?

Book a scoping call
Vindh Sharma
Vindh Sharma
Vindh Sharma is a Shopify development specialist and e-commerce strategist at Prateeksha Web Design. He writes practical guides on Shopify architecture, performance, and conversion optimisation for DTC brands.

Comments

Leave a Comment

Loading comments...