Next.js + Shopify Storefront API Caching Architecture for Flash-Sale Stability

By Milan Dhameliya · · 5 min read

Implement a resilient Next.js and Shopify Storefront API cache model with tagged revalidation, webhook invalidation, and cart-safe no-store flows.

If catalog, inventory, and cart traffic share the same cache policy, your store will either serve stale commerce data or overload origin APIs during peaks.

Where this breaks on scaling stores

  1. Teams put cache: 'no-store' everywhere and destroy throughput.
  2. Teams cache cart responses and return stale quantities or stale pricing.
  3. Webhooks are not connected to cache invalidation tags.
  4. Buyer identity/IP headers are inconsistent, causing avoidable API issues.

Data volatility model

Split storefront traffic into three cache classes:

  1. catalog for product/collection content that can tolerate minute-level staleness.
  2. inventory for low-latency stock updates.
  3. cart for strictly uncached operations.

Build a central fetch contract

type CacheClass = "catalog" | "inventory" | "cart";

type ShopifyFetchInput = {
  query: string;
  variables?: Record<string, unknown>;
  cacheClass: CacheClass;
  tags?: string[];
  buyerIp?: string;
};

const CACHE_POLICY: Record<CacheClass, {
  cache: RequestCache;
  revalidate?: number;
  tags?: string[];
}> = {
  catalog: { cache: "force-cache", revalidate: 900, tags: ["catalog"] },
  inventory: { cache: "force-cache", revalidate: 20, tags: ["inventory"] },
  cart: { cache: "no-store" }
};

Enforce no mixed caching directives

export async function shopifyFetch<T>(input: ShopifyFetchInput): Promise<T> {
  const policy = CACHE_POLICY[input.cacheClass];

  const headers: Record<string, string> = {
    "Content-Type": "application/json",
    "X-Shopify-Storefront-Access-Token": process.env.SHOPIFY_STOREFRONT_TOKEN || ""
  };

  if (input.buyerIp) {
    headers["Shopify-Storefront-Buyer-IP"] = input.buyerIp;
  }

  const requestInit: RequestInit & {
    next?: { revalidate?: number; tags?: string[] };
  } = {
    method: "POST",
    headers,
    cache: policy.cache,
    body: JSON.stringify({ query: input.query, variables: input.variables || {} })
  };

  if (policy.cache !== "no-store") {
    requestInit.next = {
      revalidate: policy.revalidate,
      tags: [...(policy.tags || []), ...(input.tags || [])]
    };
  }

  const res = await fetch(
    `${process.env.SHOPIFY_STORE_DOMAIN}/api/2026-01/graphql.json`,
    requestInit
  );

  if (!res.ok) throw new Error(`Storefront API failed: ${res.status}`);

  const json = await res.json();
  if (json.errors?.length) throw new Error(JSON.stringify(json.errors));

  return json.data as T;
}

Catalog and cart query segregation

Cached catalog query

const COLLECTION_QUERY = `
  query CollectionByHandle($handle: String!) {
    collection(handle: $handle) {
      id
      title
      products(first: 24) {
        nodes {
          id
          handle
          title
          featuredImage { url altText width height }
        }
      }
    }
  }
`;

export function getCollection(handle: string) {
  return shopifyFetch({
    query: COLLECTION_QUERY,
    variables: { handle },
    cacheClass: "catalog",
    tags: [`collection:${handle}`]
  });
}

Uncached cart mutation

const CART_LINES_ADD = `
  mutation CartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {
    cartLinesAdd(cartId: $cartId, lines: $lines) {
      cart { id totalQuantity }
      userErrors { field message }
    }
  }
`;

export function addCartLines(cartId: string, lines: Array<{ merchandiseId: string; quantity: number }>) {
  return shopifyFetch({
    query: CART_LINES_ADD,
    variables: { cartId, lines },
    cacheClass: "cart"
  });
}

Webhook-driven invalidation

Revalidate tags from Shopify webhook topics

// app/api/revalidate/shopify/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createHmac, timingSafeEqual } from "node:crypto";
import { revalidateTag } from "next/cache";

function validHmac(payload: string, received: string | null): boolean {
  if (!received) return false;
  const digest = createHmac("sha256", process.env.SHOPIFY_WEBHOOK_SECRET || "")
    .update(payload, "utf8")
    .digest("base64");

  return timingSafeEqual(Buffer.from(digest), Buffer.from(received));
}

export async function POST(req: NextRequest) {
  const raw = await req.text();

  if (!validHmac(raw, req.headers.get("x-shopify-hmac-sha256"))) {
    return NextResponse.json({ ok: false }, { status: 401 });
  }

  const topic = req.headers.get("x-shopify-topic");

  if (topic === "products/update" || topic === "products/delete") {
    revalidateTag("catalog", "max");
  }

  if (topic === "inventory_levels/update") {
    revalidateTag("inventory", "max");
  }

  return NextResponse.json({ ok: true });
}

UI path: Settings > Notifications > Webhooks.

Technical Checklist

SEO Checklist