SWR in-memory fetch cache for VTEX API responses in @decocms/apps. Ported from deco-cx/deco runtime/fetch/fetchCache.ts. Provides in-flight deduplication + stale-while-revalidate for all VTEX GET requests. Covers fetchWithCache utility, vtexCachedFetch client function, LRU eviction, TTL by HTTP status, integration with intelligentSearch and cross-selling calls. Use when adding caching to VTEX API calls, debugging stale responses, or understanding how the fetch cache layer works.
Server-side SWR cache for all VTEX GET API responses. Ported from deco-cx/deco runtime/fetch/fetchCache.ts and adapted for @decocms/apps on TanStack Start.
fetchWithCache, vtexCachedFetch, and createCachedLoaderSite Setup
└→ createCachedLoader (loader-level SWR, 30-120s TTL)
└→ vtexCachedFetch (HTTP-level SWR, 3min TTL)
└→ fetchWithCache (core cache engine)
└→ _fetch (instrumented fetch)
| Layer | File | Scope |
|---|
| TTL |
|---|
createCachedLoader | deco-start/src/sdk/cachedLoader.ts | Loader result (parsed + transformed) | 30-120s per loader |
vtexCachedFetch | apps-start/vtex/client.ts | Raw HTTP JSON response | 3 min (200), 10s (404) |
fetchWithCache | apps-start/vtex/utils/fetchCache.ts | Core SWR + dedup engine | Status-based |
fetchWithCache (vtex/utils/fetchCache.ts)opts.ttlimport { fetchWithCache, FetchCacheOptions } from "@decocms/apps/vtex/utils/fetchCache";
// Basic usage
const data = await fetchWithCache<ProductType>(
fullUrl, // Cache key (typically the full URL)
() => fetch(fullUrl, init), // Fetch callback (returns Response)
{ ttl: 60_000 }, // Optional: override TTL (1 min)
);
Call fetchWithCache(key, doFetch)
├→ Entry exists & fresh? → return cached body
├→ Entry exists & stale? → return stale, fire background refresh
├→ Entry missing, inflight exists? → await inflight Promise
└→ Entry missing, no inflight → execute doFetch(), cache result
const TTL_BY_STATUS: Record<string, number> = {
"2xx": 180_000, // 3 min — success responses
"404": 10_000, // 10s — not found (may become available)
"5xx": 0, // never cache server errors
};
Non-ok responses (status >= 400) throw an error. They are NOT cached. The error propagates to the caller, who should .catch() gracefully:
const data = await fetchWithCache<T>(url, doFetch).catch(() => fallback);
vtexCachedFetch (vtex/client.ts)Convenience wrapper that routes GET requests through fetchWithCache:
import { vtexCachedFetch } from "@decocms/apps/vtex";
// Automatically uses SWR cache for GET
const products = await vtexCachedFetch<Product[]>(
`/api/catalog_system/pub/products/search/${slug}/p`,
);
// Non-GET falls through to regular vtexFetch
const result = await vtexCachedFetch<OrderForm>(
`/api/checkout/pub/orderForms/simulation`,
{ method: "POST", body: JSON.stringify(items) },
);
const pageType = await vtexCachedFetch<PageType>(
`/api/catalog_system/pub/portal/pagetype/${term}`,
undefined,
{ cacheTTL: 300_000 }, // 5 min for page types
);
vtexCachedFetch (Current)| Module | Endpoint | Before | After |
|---|---|---|---|
slugCache.ts | search/{slug}/p | Manual inflight Map + 5s timeout | vtexCachedFetch SWR 3min |
relatedProducts.ts | crossselling/{type}/{id} | Manual crossSellingInflight Map | vtexCachedFetch SWR 3min |
productDetailsPage.ts | Kit items search | Plain vtexFetch | vtexCachedFetch SWR 3min |
client.ts cachedPageType | pagetype/{term} | Manual inflight Map | vtexCachedFetch SWR 3min |
fetchWithCache Directly| Module | Endpoint | Notes |
|---|---|---|
client.ts intelligentSearch | IS product_search, facets | Wrapped inline, uses default TTL |
deco-cx/deco fetchCache.ts| Feature | deco-cx/deco | @decocms/apps |
|---|---|---|
| Storage | CacheStorage (Web Cache API) | In-memory Map (LRU) |
| Persistence | Disk-backed (Deno CacheStorage) | Process-lifetime only |
| Max entries | Unlimited (disk) | 500 (memory) |
| TTL source | HTTP Cache-Control headers | Status-based defaults |
| SWR | stale-while-revalidate header parsing | Manual background refresh |
| Dedup | Separate singleFlight wrapper | Built into fetchWithCache |
| Redis/FS tiers | Yes (tiered.ts: LRU → FS → Redis) | No — single in-memory tier |
Cloudflare Workers don't have persistent storage APIs accessible during SSR. The Cache API (caches.default) is for edge HTTP responses, not arbitrary data. In-memory with LRU is the practical choice for Workers.
import { getFetchCacheStats, clearFetchCache } from "@decocms/apps/vtex/utils/fetchCache";
console.log(getFetchCacheStats());
// { entries: 42, inflight: 0 }
clearFetchCache(); // Useful after decofile hot-reload
With instrumented fetch (createInstrumentedFetch("vtex")), look for timing:
[vtex] GET ... log appears (response served from cache)[vtex] GET ... followed by [vtex] 200 GET ... Xms[vtex] GET ... appears AFTER the response was already servedStale data after CMS update: Wait 3 min for TTL to expire, or restart the dev server to clear in-memory cache.
Cache not working in dev: fetchWithCache works in both dev and prod. Unlike createCachedLoader which skips SWR in dev (only dedup), fetchWithCache always caches.
POST requests not cached: By design — vtexCachedFetch only caches GET. Use usePriceSimulationBatch for simulation POST optimization.
// Before — no cache
const data = await vtexFetch<MyType>(`/api/my-endpoint/${id}`);
// After — with SWR cache
const data = await vtexCachedFetch<MyType>(`/api/my-endpoint/${id}`);
// With custom TTL
const data = await vtexCachedFetch<MyType>(
`/api/my-endpoint/${id}`,
undefined,
{ cacheTTL: 60_000 }, // 1 min
);
For non-VTEX APIs, use fetchWithCache directly:
import { fetchWithCache } from "@decocms/apps/vtex/utils/fetchCache";
const data = await fetchWithCache<MyType>(url, () => fetch(url, init));
| Skill | Purpose |
|---|---|
deco-api-call-dedup | Higher-level dedup patterns (slugCache, batching, PLP filtering) |
deco-cms-layout-caching | Layout section caching (works on top of fetch cache) |
deco-edge-caching | Cloudflare edge caching (HTTP level, outside the Worker) |
deco-tanstack-storefront-patterns | General storefront patterns + createCachedLoader |