Typical situation: you've launched a Medusa backend, but the Next.js frontend throws hydration mismatches, the cart doesn't persist after reload, and SEO meta tags aren't generated. 80% of performance issues stem from N+1 queries to the Store API. Average page load time drops by 40% after proper batch request configuration (see Store API). Our team has 5 years of experience in headless e-commerce, with over 50 successful projects, 15+ of which are on Medusa. Contact us for a free consultation — we'll help you choose the optimal stack.
Main Medusa Integration Problems
N+1 queries to the Store API when building a catalog — each product fetches variants and prices separately, killing LCP. Solution: we implement batch requests via medusaClient.store.product.list with fields *variants,*variants.prices. This reduces HTTP calls from 100 to 5 when loading a catalog of 50 products.
Cart context loss during SSR — the client sees an empty cart on first load. We fix this by saving cart_id in localStorage and restoring it via a Cart Context.
Slow checkout — due to synchronous API calls. We optimize by parallelizing address and shipping method requests.
Why Next.js Is the Optimal Choice for Medusa
Next.js with App Router offers an ideal balance of dynamism and SEO: product pages generate as static (generateStaticParams), while the cart and checkout work via 'use client'. Unlike Gatsby, where the cart requires a separate SPA island, Next.js allows flexible switching between SSR, SSG, and ISR. This reduces LCP by 30% and simplifies maintenance. Medusa documentation confirms that Next.js is the recommended framework for headless e-commerce.
Setting Up Medusa JS SDK in Next.js
Install the package and types:
npm install @medusajs/js-sdk @medusajs/types Create a client with the base URL, auth type, and publishableKey:
// lib/medusa/client.ts import Medusa from '@medusajs/js-sdk'; export const medusaClient = new Medusa({ baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!, auth: { type: 'session', // or 'jwt' for headless }, publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY, }); Use the client in Server Components for SSR or in client components for dynamism. A typical mistake: not providing a publishableKey — the SDK returns a 401.
Restoring the Cart on First Load
Create a context that restores the cart from localStorage on initialization and provides addItem, removeItem, updateItem methods:
// context/cart-context.tsx 'use client'; import { createContext, useContext, useEffect, useState } from 'react'; import { medusaClient } from '@/lib/medusa/client'; import type { HttpTypes } from '@medusajs/types'; type CartContextType = { cart: HttpTypes.StoreCart | null; addItem: (variantId: string, quantity: number) => Promise<void>; removeItem: (lineItemId: string) => Promise<void>; updateItem: (lineItemId: string, quantity: number) => Promise<void>; isLoading: boolean; }; const CartContext = createContext<CartContextType | null>(null); export function CartProvider({ children }: { children: React.ReactNode }) { const [cart, setCart] = useState<HttpTypes.StoreCart | null>(null); const [isLoading, setIsLoading] = useState(false); useEffect(() => { const cartId = localStorage.getItem('cart_id'); if (cartId) { medusaClient.store.cart.retrieve(cartId) .then(({ cart }) => setCart(cart)) .catch(() => localStorage.removeItem('cart_id')); } }, []); const addItem = async (variantId: string, quantity: number) => { setIsLoading(true); try { let currentCart = cart; if (!currentCart) { const { cart: newCart } = await medusaClient.store.cart.create({ region_id: process.env.NEXT_PUBLIC_MEDUSA_REGION_ID, }); localStorage.setItem('cart_id', newCart.id); currentCart = newCart; } const { cart: updatedCart } = await medusaClient.store.cart.createLineItem( currentCart.id, { variant_id: variantId, quantity } ); setCart(updatedCart); } finally { setIsLoading(false); } }; const removeItem = async (lineItemId: string) => { if (!cart) return; setIsLoading(true); try { const { cart: updatedCart } = await medusaClient.store.cart.deleteLineItem( cart.id, lineItemId ); setCart(updatedCart); } finally { setIsLoading(false); } }; const updateItem = async (lineItemId: string, quantity: number) => { if (!cart) return; const { cart: updatedCart } = await medusaClient.store.cart.updateLineItem( cart.id, lineItemId, { quantity } ); setCart(updatedCart); }; return ( <CartContext.Provider value={{ cart, addItem, removeItem, updateItem, isLoading }}> {children} </CartContext.Provider> ); } export const useCart = () => { const ctx = useContext(CartContext); if (!ctx) throw new Error('useCart must be used within CartProvider'); return ctx; }; Implementing Multi-Region in Next.js and Medusa
Use middleware to determine the region by geolocation and pass region_id with each request:
// middleware.ts import { NextRequest, NextResponse } from 'next/server'; const REGION_MAP: Record<string, string> = { RU: process.env.MEDUSA_REGION_RU!, BY: process.env.MEDUSA_REGION_BY!, DE: process.env.MEDUSA_REGION_EU!, DEFAULT: process.env.MEDUSA_REGION_DEFAULT!, }; export function middleware(request: NextRequest) { const country = request.geo?.country ?? 'DEFAULT'; const regionId = REGION_MAP[country] ?? REGION_MAP.DEFAULT; const response = NextResponse.next(); response.cookies.set('medusa_region', regionId, { maxAge: 60 * 60 * 24, sameSite: 'lax', }); return response; } What to Choose: Gatsby or Next.js?
If the assortment rarely changes — Gatsby with the gatsby-source-medusa plugin generates static pages that load instantly. The cart is implemented via a separate client layer, and checkout is redirected to the Medusa server. This is cheaper and faster for simple stores.
// gatsby-config.ts import type { GatsbyConfig } from 'gatsby'; const config: GatsbyConfig = { plugins: [ { resolve: 'gatsby-source-medusa', options: { storeUrl: process.env.GATSBY_MEDUSA_BACKEND_URL, publishableApiKey: process.env.GATSBY_MEDUSA_PUBLISHABLE_KEY, entities: ['products', 'collections', 'regions'], batchSize: 100, }, }, ], }; export default config; | Parameter | Next.js | Gatsby |
|---|---|---|
| Cart dynamism | React Server + Client | Separate SPA island |
| SEO | SSG/ISR/SSR per page | Full SSG |
| Build time | Instant (ISR) | Long with >1000 products |
| Flexibility | High | Medium |
Next.js wins in performance and team productivity. Gatsby is best when static generation is critical.
Additional recommendations for choosing
If you have more than 10,000 products, consider Next.js with ISR to avoid rebuilding the entire catalog on every change. Gatsby is suitable for catalogs up to 1,000 products with infrequent updates.Integration Timelines
| Task type | Estimated timeline |
|---|---|
| Next.js Storefront Starter + connection + customization | 2–3 weeks |
| Custom Next.js frontend from scratch (App Router, SSR/SSG, cart, checkout) | 6–10 weeks |
| Gatsby SSG catalog with dynamic cart (hybrid) | 4–6 weeks |
| Multi-region store with URL and content localization | +2–3 weeks to base estimate |
Cost is calculated individually and fixed at the contract stage.
What's Included in the Work
- Architectural documentation (data schema, request flow)
- Code writing and code review
- CI/CD setup and deployment to Vercel/hosting
- Training your team on using the Medusa SDK
- Guarantee support for 1 month after launch
Process of Work
- Analysis — we study your store's specifics and frontend requirements.
- Design — we choose the stack (Next.js/Gatsby), design data and component architecture.
- Implementation — we write code, configure SDK, cart, checkout, multi-region.
- Testing — we verify compliance with Core Web Vitals and buyer scenarios.
- Deployment — we set up CI/CD, deploy to Vercel or your hosting.
Order Medusa integration — get a working store from scratch in 2 weeks. Get a consultation on your project — contact us for a free estimate.







