--- title: Caching third-party API data with Hydrogen and Oxygen description: Learn how to cache third-party data queries with Hydrogen and Oxygen. source_url: html: https://shopify.dev/docs/storefronts/headless/hydrogen/caching/third-party md: https://shopify.dev/docs/storefronts/headless/hydrogen/caching/third-party.md --- ExpandOn this page * [Hydrogen’s built-in with​Cache utility](https://shopify.dev/docs/storefronts/headless/hydrogen/caching/third-party#hydrogens-built-in-withcache-utility) * [Custom cache abstractions](https://shopify.dev/docs/storefronts/headless/hydrogen/caching/third-party#custom-cache-abstractions) * [Manual caching](https://shopify.dev/docs/storefronts/headless/hydrogen/caching/third-party#manual-caching) # Caching third-party API data with Hydrogen and Oxygen Note This guide might not be compatible with features introduced in Hydrogen version 2025-05 and above. Check the latest [documentation](https://shopify.dev/docs/api/hydrogen) if you encounter any issues. The API client built into Hydrogen includes [caching strategies for Storefront API data](https://shopify.dev/docs/storefronts/headless/hydrogen/caching). However, if you make `fetch` requests to third-party APIs in your Hydrogen app, then the following behavior occurs: * HTTP GET responses are cached according to their response headers. * POST requests aren't cached. There are several ways to manage caching of third-party data with Hydrogen and Oxygen: 1. [Hydrogen’s built-in `withCache` utility](#hydrogen-s-built-in-withcache-utility) (recommended) 2. [Creating custom abstractions](#custom-cache-abstractions) 3. [Caching content manually](#manual-caching) Note If you [host your Hydrogen app on another provider](https://shopify.dev/docs/storefronts/headless/hydrogen/deployments/self-hosting) instead of Oxygen, then caching might work differently. Consult your provider for details on its caching capabilities. *** ## Hydrogen’s built-in with​Cache utility Hydrogen includes a [`createWithCache`](https://shopify.dev/docs/api/hydrogen/latest/utilities/caching/createwithcache) utility to support caching third-party API calls. This utility wraps an arbitrary number of sub-requests under a single cache key. ### Step 1: Create and inject the utility function To start, create a `withCache` function in your project server file and pass it as part of the Remix context. The following example shows how `withCache` works with Oxygen: ## Create the withCache utility ## /server.js ```jsx import {createStorefrontClient, createWithCache} from '@shopify/hydrogen'; import {createRequestHandler} from '@shopify/remix-oxygen'; export default { async fetch(request, env, executionContext) { const cache = await caches.open('hydrogen'); const waitUntil = (promise) => executionContext.waitUntil(promise); const {storefront} = createStorefrontClient({ cache, waitUntil, // ... }); // Create withCache object const withCache = createWithCache({cache, waitUntil, request}); const handleRequest = createRequestHandler({ build: remixBuild, mode: process.env.NODE_ENV, // Pass withCache to the Remix context getLoadContext: () => ({storefront, withCache, waitUntil}), }); return handleRequest(request); }, }; ``` ```tsx import {createStorefrontClient, createWithCache} from '@shopify/hydrogen'; import {createRequestHandler} from '@shopify/remix-oxygen'; export default { async fetch(request: Request, env: Env, executionContext: ExecutionContext) { const cache = await caches.open('hydrogen'); const waitUntil = (promise: Promise) => executionContext.waitUntil(promise); const {storefront} = createStorefrontClient({ cache, waitUntil, // ... }); // Create withCache object const withCache = createWithCache({cache, waitUntil, request}); const handleRequest = createRequestHandler({ build: remixBuild, mode: process.env.NODE_ENV, // Pass withCache to the Remix context getLoadContext: () => ({storefront, withCache, waitUntil}), }); return handleRequest(request); }, }; ``` ```tsx /** * For TypeScript projects, import Hydrogen’s included `withCache` types * in the Remix context by adding them to your Remix type declaration file. */ import type {Storefront, WithCache} from '@shopify/hydrogen'; declare module '@shopify/remix-oxygen' { export interface AppLoadContext { storefront: Storefront; withCache: WithCache; waitUntil: (promise: Promise) => void; } } ``` ### Step 2: Call `withCache.fetch` in Remix loaders and actions After you pass the utility function to the Remix context, `withCache` is available in all Remix loaders and actions. In the following example, the `withCache.fetch` function wraps a standard `fetch` query to a third-party CMS: ## Cache a sub-request to a third-party API using withCache ## /app/routes/pages/example.jsx ```jsx const CMS_API_ENDPOINT = 'https://example-cms.com/api'; export async function loader({request, context}) { { storefront, withCache } = context; const query = `query { product { id } }`; /** * The cache key is used to uniquely identify the stored value in cache. * If caching data for logged-in users, then make sure to add something * unique to the user in the cache key, such as their email address. */ const cacheKey = [CMS_API_ENDPOINT, query]; const {data} = await withCache.fetch( CMS_API_ENDPOINT, // URL to fetch { // Fetch options method: 'POST', body: JSON.stringify({query}), headers: {'Content-Type': 'application/json'}, }, { // Caching options cacheKey, cacheStrategy: storefront.CacheLong(), shouldCacheResponse: () => true, // withCache.fetch will only cache when response.ok }, ); return {idFromCMS: data.product.id}; } ``` ```tsx import type {LoaderFunctionArgs} from '@shopify/remix-oxygen'; const CMS_API_ENDPOINT = 'https://example-cms.com/api'; export async function loader({request, context: {storefront, withCache}}: LoaderFunctionArgs) { const query = `query { product { id } }`; /** * The cache key is used to uniquely identify the stored value in cache. * If caching data for logged-in users, then make sure to add something * unique to the user in the cache key, such as their email address. */ const cacheKey = [CMS_API_ENDPOINT, query]; const {data} = await withCache.fetch<{product: {id: string}}>( CMS_API_ENDPOINT, // URL to fetch { // Fetch options method: 'POST', body: JSON.stringify({query}), headers: {'Content-Type': 'application/json'}, }, { // Caching options cacheKey, cacheStrategy: storefront.CacheLong(), shouldCacheResponse: () => true, // withCache.fetch will only cache when response.ok }, ); return {idFromCMS: data!.product.id}; } ``` *** ## Custom cache abstractions Instead of using `withCache.fetch` directly in your routes, you can also create custom abstractions around it. For example, you can make your own CMS fetcher and inject it in the Remix context. You can create as many abstractions as needed for your third-party APIs, and they will be available in Remix loaders and actions. For TypeScript projects, you should add types accordingly in the `remix.env.d.ts` file. ## Create a custom abstraction ## /server.js ```jsx import {createStorefrontClient, createWithCache} from '@shopify/hydrogen'; import {createRequestHandler} from '@shopify/remix-oxygen'; export default { async fetch(request, env, executionContext) { const cache = await caches.open('hydrogen'); const waitUntil = (promise) => executionContext.waitUntil(promise); const {storefront} = createStorefrontClient({ cache, waitUntil, // ... }); const withCache = createWithCache({cache, waitUntil, request}); const fetchMyCMS = ( query: string, cacheStrategy = storefront.CacheLong(), ) => { const CMS_API_ENDPOINT = 'https://example-cms.com/api'; const cacheKey = [CMS_API_ENDPOINT, query]; return withCache.fetch( CMS_API_ENDPOINT, { method: 'POST', body: JSON.stringify({query}), headers: {'Content-Type': 'application/json'}, }, { cacheKey, cacheStrategy, // Cache if there are no data errors or specific data that make this result not suited for caching shouldCacheResponse: (result) => !(result?.errors || result?.isLoggedIn), }, ); }; const handleRequest = createRequestHandler({ build: remixBuild, mode: process.env.NODE_ENV, getLoadContext: () => ({storefront, fetchMyCMS, waitUntil}), }); return handleRequest(request); }, }; ``` ```tsx import {createStorefrontClient, createWithCache} from '@shopify/hydrogen'; import {createRequestHandler} from '@shopify/remix-oxygen'; export default { async fetch(request: Request, env: Env, executionContext: ExecutionContext) { const cache = await caches.open('hydrogen'); const waitUntil = (promise: Promise) => executionContext.waitUntil(promise); const {storefront} = createStorefrontClient({ cache, waitUntil, // ... }); const withCache = createWithCache({cache, waitUntil, request}); const fetchMyCMS = ( query: string, cacheStrategy = storefront.CacheLong(), ) => { const CMS_API_ENDPOINT = 'https://example-cms.com/api'; const cacheKey = [CMS_API_ENDPOINT, query]; return withCache.fetch( CMS_API_ENDPOINT, { method: 'POST', body: JSON.stringify({query}), headers: {'Content-Type': 'application/json'}, }, { cacheKey, cacheStrategy, // Cache if there are no data errors or specific data that make this result not suited for caching shouldCacheResponse: (result: My3PDataResponse) => !(result?.errors || result?.isLoggedIn), }, ); }; const handleRequest = createRequestHandler({ build: remixBuild, mode: process.env.NODE_ENV, getLoadContext: () => ({storefront, fetchMyCMS, waitUntil}), }); return handleRequest(request); }, }; ``` Alternatively, if you need to do include extra logic within the custom cache abstraction itself, there is `withCache.run` ## Create a custom abstraction with withCache run ## /server.js ```jsx import {createStorefrontClient, createWithCache} from '@shopify/hydrogen'; import {createRequestHandler} from '@shopify/remix-oxygen'; export default { async fetch(request, env, executionContext) { const cache = await caches.open('hydrogen'); const waitUntil = (promise) => executionContext.waitUntil(promise); const {storefront} = createStorefrontClient({ cache, waitUntil, // ... }); const withCache = createWithCache({cache, waitUntil, request}); const fetchMyCMS = ( query: string, cacheStrategy = storefront.CacheLong(), ) => { const CMS_API_ENDPOINT = 'https://example-cms.com/api'; const cacheKey = [CMS_API_ENDPOINT, query]; return withCache.run( { cacheKey, cacheStrategy, // Cache if there are no data errors shouldCacheResult: (result) => result.errors.length === 0, }, async () => { const response = await fetch(CMS_API_ENDPOINT, { method: 'POST', body: JSON.stringify({query}), headers: {'Content-Type': 'application/json'}, }); if (!response.ok) return {errors: ['Something went wrong']}; const {product} = await response.json(); return {id: product.id, errors: []}; }, ); }; const handleRequest = createRequestHandler({ build: remixBuild, mode: process.env.NODE_ENV, getLoadContext: () => ({storefront, fetchMyCMS, waitUntil}), }); return handleRequest(request); }, }; ``` ```tsx import {createStorefrontClient, createWithCache} from '@shopify/hydrogen'; import {createRequestHandler} from '@shopify/remix-oxygen'; export default { async fetch(request: Request, env: Env, executionContext: ExecutionContext) { const cache = await caches.open('hydrogen'); const waitUntil = (promise: Promise) => executionContext.waitUntil(promise); const {storefront} = createStorefrontClient({ cache, waitUntil, // ... }); const withCache = createWithCache({cache, waitUntil, request}); const fetchMyCMS = ( query: string, cacheStrategy = storefront.CacheLong(), ) => { const CMS_API_ENDPOINT = 'https://example-cms.com/api'; const cacheKey = [CMS_API_ENDPOINT, query]; return withCache.run( { cacheKey, cacheStrategy, // Cache if there are no data errors shouldCacheResult: (result: My3PDataResponse) => result.errors.length === 0, }, async () => { const response = await fetch(CMS_API_ENDPOINT, { method: 'POST', body: JSON.stringify({query}), headers: {'Content-Type': 'application/json'}, }); if (!response.ok) return {errors: ['Something went wrong']}; const {product} = await response.json<{product: {id: string}}>(); return {id: product.id, errors: []}; }, ); }; const handleRequest = createRequestHandler({ build: remixBuild, mode: process.env.NODE_ENV, getLoadContext: () => ({storefront, fetchMyCMS, waitUntil}), }); return handleRequest(request); }, }; ``` ### Overriding default caching behavior By default, [`withCache.fetch`](https://shopify.dev/docs/api/hydrogen/latest/utilities/caching/createwithcache#createWithCache-returns) will cache successful fetches (i.e. those where `response.ok` is true), and [`withCache.run`](https://shopify.dev/docs/api/hydrogen/latest/utilities/caching/createwithcache#createWithCache-returns) will always cache the result. This may not always be desirable, for example a CMS query may technically respond with an `ok` status code, but the response data might still contain errors. In these cases, you can override the default caching behavior. To override the default caching behavior for `withCache.fetch`, you need to use `shouldCacheResponse`, which takes 2 params: the response data, and the `Response` object itself: ## Custom cache behavior for withCache fetch ## /server.js ```jsx withCache.fetch( CMS_API_ENDPOINT, { method: 'POST', body: JSON.stringify({query}), headers: {'Content-Type': 'application/json'}, }, { cacheKey, cacheStrategy, shouldCacheResponse: (result, response) => response.status === 200 && !result.isLoggedIn, // Do not cache if the buyer is in a logged in state }, ); ``` ```tsx withCache.fetch( CMS_API_ENDPOINT, { method: 'POST', body: JSON.stringify({query}), headers: {'Content-Type': 'application/json'}, }, { cacheKey, cacheStrategy, shouldCacheResponse: (result: My3PDataResponse, response: Response) => response.status === 200 && !result.isLoggedIn, // Do not cache if the buyer is in a logged in state }, ); ``` To override the default caching behavior for `withCache.run`, you need to use `shouldCacheResult`, which only takes 1 param: the result of the inner function: ## Custom cache behavior for withCache run ## /server.js ```jsx withCache.run( { cacheKey, cacheStrategy, shouldCacheResult: (result) => result.errors.length === 0, }, async () => { ... }, ); ``` ```tsx withCache.run( { cacheKey, cacheStrategy, shouldCacheResult: (result: My3PDataResponse) => result.errors.length === 0, }, async () => { ... }, ); ``` *** ## Manual caching As an alternative to the `withCache` utility, you can also directly use the `cache` instance that's passed to the Storefront client and available in `storefront.cache`. This cache instance follows the [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache). Using the cache instance directly is a low-level approach and you need to handle all the cases and features manually, including error handling and stale-while-revalidate. The following example shows how to cache a request to a third-party API with Oxygen: ## Cache a sub-request to a third-party API using the cache instance ## /app/routes/pages/example.jsx ```jsx const CMS_API_ENDPOINT = 'https://my-cms.com/api'; export async function loader({request, context: {storefront, waitUntil}}) { const body = JSON.stringify(Object.fromEntries(new URL(request.url).searchParams.entries())); // Create a new request based on a unique key representing the API request. // This could use any unique URL that depends on the API request. // For example, it could concatenate its text body or its sha256 hash. const cacheUrl = new URL(CMS_API_ENDPOINT); cacheUrl.pathname = '/cache' + cacheUrl.pathname + generateUniqueKeyFrom(body); const cacheKey = new Request(cacheUrl.toString()); // Check if there's a match for this key. let response = await storefront.cache.match(cacheKey); if (!response) { // Since there's no match, fetch a fresh response. response = await fetch(CMS_API_ENDPOINT, {body, method: 'POST'}); // Make the response mutable. response = new Response(response.body, response); // Add caching headers to the response. response.headers.set('Cache-Control', 'public, max-age=10') // Store the response in cache to be re-used the next time. waitUntil(storefront.cache.put(cacheKey, response.clone())); } return response; } ``` ```tsx import type {LoaderArgs} from '@shopify/remix-oxygen'; const CMS_API_ENDPOINT = 'https://my-cms.com/api'; export async function loader({request, context: {storefront, waitUntil}}: LoaderArgs) { const body = JSON.stringify(Object.fromEntries(new URL(request.url).searchParams.entries())); // Create a new request based on a unique key representing the API request. // This could use any unique URL that depends on the API request. // For example, it could concatenate its text body or its sha256 hash. const cacheUrl = new URL(CMS_API_ENDPOINT); cacheUrl.pathname = '/cache' + cacheUrl.pathname + generateUniqueKeyFrom(body); const cacheKey = new Request(cacheUrl.toString()); // Check if there's a match for this key. let response = await storefront.cache.match(cacheKey); if (!response) { // Since there's no match, fetch a fresh response. response = await fetch(CMS_API_ENDPOINT, {body, method: 'POST'}); // Make the response mutable. response = new Response(response.body, response); // Add caching headers to the response. response.headers.set('Cache-Control', 'public, max-age=10') // Store the response in cache to be re-used the next time. waitUntil(storefront.cache.put(cacheKey, response.clone())); } return response; } ``` *** * [Hydrogen’s built-in with​Cache utility](https://shopify.dev/docs/storefronts/headless/hydrogen/caching/third-party#hydrogens-built-in-withcache-utility) * [Custom cache abstractions](https://shopify.dev/docs/storefronts/headless/hydrogen/caching/third-party#custom-cache-abstractions) * [Manual caching](https://shopify.dev/docs/storefronts/headless/hydrogen/caching/third-party#manual-caching)