[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"LayoutTheSiteFooter_1yyBGXy6PpKR7pXh5WpcU4gY8HkIFkmmeLbXg8mpkY":3,"\u002Fblog\u002Fstop-caching-everyhing-by-hand":10},["Island",4],{"key":5,"params":6,"result":8},"LayoutTheSiteFooter_1yyBGXy6PpKR7pXh5WpcU4gY8HkIFkmmeLbXg8mpkY",{"props":7},"{}",{"head":9},{},{"id":11,"title":12,"body":13,"date":591,"description":592,"extension":593,"meta":594,"navigation":595,"path":596,"seo":597,"stem":598,"tags":599,"__hash__":604},"blog\u002Fblog\u002Fstop-caching-everyhing-by-hand.md","Stop Caching Everything By Hand: Let Route Rules Do the Work",{"type":14,"value":15,"toc":585},"minimark",[16,20,33,40,45,48,239,253,257,264,380,383,523,534,538,549,555,559,566,573,576,581],[17,18,12],"h1",{"id":19},"stop-caching-everything-by-hand-let-route-rules-do-the-work",[21,22,23,24,28,29,32],"p",{},"When I first wired up a TMDB proxy for a Nuxt project running on Cloudflare Workers, my instinct was the same as most developers': write a caching layer by hand. Check a KV key, if it's missing fetch fresh data, write it back, set a TTL, repeat. It works. It also turns into a mess fast — cache invalidation logic scattered across handlers, config values duplicated everywhere, and a ",[25,26,27],"code",{},"defineCachedEventHandler"," fighting with ",[25,30,31],{},"useRuntimeConfig(event)"," in ways that are miserable to debug.",[21,34,35,36],{},"Here's the tip: ",[37,38,39],"strong",{},"let your framework's routing layer own caching, not your handler code.",[41,42,44],"h2",{"id":43},"the-problem-with-caching-inside-the-handler","The problem with caching inside the handler",[21,46,47],{},"A typical first attempt looks like this:",[49,50,55],"pre",{"className":51,"code":52,"language":53,"meta":54,"style":54},"language-ts shiki shiki-themes vitesse-light vitesse-dark","export default defineCachedEventHandler(async (event) => {\n  const config = useRuntimeConfig(event)\n  const res = await $fetch(`https:\u002F\u002Fapi.themoviedb.org\u002F3\u002Fmovie\u002Fpopular`, {\n    headers: { Authorization: `Bearer ${config.tmdbToken}` }\n  })\n  return res\n}, {\n  maxAge: 60 * 10\n})\n","ts","",[25,56,57,97,119,152,192,198,207,215,233],{"__ignoreMap":54},[58,59,62,66,69,73,77,81,84,88,91,94],"span",{"class":60,"line":61},"line",1,[58,63,65],{"class":64},"sTPum","export",[58,67,68],{"class":64}," default",[58,70,72],{"class":71},"s_xSY"," defineCachedEventHandler",[58,74,76],{"class":75},"si6no","(",[58,78,80],{"class":79},"s5TCs","async",[58,82,83],{"class":75}," (",[58,85,87],{"class":86},"s9nN2","event",[58,89,90],{"class":75},")",[58,92,93],{"class":75}," =>",[58,95,96],{"class":75}," {\n",[58,98,100,103,106,109,112,114,116],{"class":60,"line":99},2,[58,101,102],{"class":79},"  const ",[58,104,105],{"class":86},"config",[58,107,108],{"class":75}," =",[58,110,111],{"class":71}," useRuntimeConfig",[58,113,76],{"class":75},[58,115,87],{"class":86},[58,117,118],{"class":75},")\n",[58,120,122,124,127,129,132,135,137,141,145,147,150],{"class":60,"line":121},3,[58,123,102],{"class":79},[58,125,126],{"class":86},"res",[58,128,108],{"class":75},[58,130,131],{"class":64}," await",[58,133,134],{"class":71}," $fetch",[58,136,76],{"class":75},[58,138,140],{"class":139},"scnC2","`",[58,142,144],{"class":143},"spP0B","https:\u002F\u002Fapi.themoviedb.org\u002F3\u002Fmovie\u002Fpopular",[58,146,140],{"class":139},[58,148,149],{"class":75},",",[58,151,96],{"class":75},[58,153,155,159,162,165,168,170,173,176,178,181,184,187,189],{"class":60,"line":154},4,[58,156,158],{"class":157},"sHLBJ","    headers",[58,160,161],{"class":75},": { ",[58,163,164],{"class":157},"Authorization",[58,166,167],{"class":75},": ",[58,169,140],{"class":139},[58,171,172],{"class":143},"Bearer ",[58,174,175],{"class":64},"${",[58,177,105],{"class":143},[58,179,180],{"class":75},".",[58,182,183],{"class":143},"tmdbToken",[58,185,186],{"class":64},"}",[58,188,140],{"class":139},[58,190,191],{"class":75}," }\n",[58,193,195],{"class":60,"line":194},5,[58,196,197],{"class":75},"  })\n",[58,199,201,204],{"class":60,"line":200},6,[58,202,203],{"class":64},"  return",[58,205,206],{"class":86}," res\n",[58,208,210,213],{"class":60,"line":209},7,[58,211,212],{"class":75},"},",[58,214,96],{"class":75},[58,216,218,221,223,227,230],{"class":60,"line":217},8,[58,219,220],{"class":157},"  maxAge",[58,222,167],{"class":75},[58,224,226],{"class":225},"sqbOQ","60",[58,228,229],{"class":79}," *",[58,231,232],{"class":225}," 10\n",[58,234,236],{"class":60,"line":235},9,[58,237,238],{"class":75},"})\n",[21,240,241,242,244,245,247,248,252],{},"Looks fine until you actually deploy it. ",[25,243,27],{}," wraps your handler in its own execution context, and depending on your Nitro preset, ",[25,246,31],{}," inside that wrapped context doesn't always resolve the way you'd expect. You end up debugging ",[249,250,251],"em",{},"why"," your config is undefined instead of building features — and the caching logic is now tangled up with your business logic, which means every route that needs caching needs its own copy of this dance.",[41,254,256],{"id":255},"move-it-up-a-layer","Move it up a layer",[21,258,259,260,263],{},"Nitro's ",[25,261,262],{},"routeRules"," let you declare caching behavior for a path pattern, completely separate from the handler that serves it:",[49,265,267],{"className":51,"code":266,"language":53,"meta":54,"style":54},"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  routeRules: {\n    '\u002Fapi\u002Ftmdb\u002F**': {\n      cache: {\n        maxAge: 60 * 10,\n        staleMaxAge: 60 * 60,\n        swr: true\n      }\n    }\n  }\n})\n",[25,268,269,275,287,295,308,315,332,348,358,363,369,375],{"__ignoreMap":54},[58,270,271],{"class":60,"line":61},[58,272,274],{"class":273},"snYqZ","\u002F\u002F nuxt.config.ts\n",[58,276,277,279,281,284],{"class":60,"line":99},[58,278,65],{"class":64},[58,280,68],{"class":64},[58,282,283],{"class":71}," defineNuxtConfig",[58,285,286],{"class":75},"({\n",[58,288,289,292],{"class":60,"line":121},[58,290,291],{"class":157},"  routeRules",[58,293,294],{"class":75},": {\n",[58,296,297,300,303,306],{"class":60,"line":154},[58,298,299],{"class":139},"    '",[58,301,302],{"class":143},"\u002Fapi\u002Ftmdb\u002F**",[58,304,305],{"class":139},"'",[58,307,294],{"class":75},[58,309,310,313],{"class":60,"line":194},[58,311,312],{"class":157},"      cache",[58,314,294],{"class":75},[58,316,317,320,322,324,326,329],{"class":60,"line":200},[58,318,319],{"class":157},"        maxAge",[58,321,167],{"class":75},[58,323,226],{"class":225},[58,325,229],{"class":79},[58,327,328],{"class":225}," 10",[58,330,331],{"class":75},",\n",[58,333,334,337,339,341,343,346],{"class":60,"line":209},[58,335,336],{"class":157},"        staleMaxAge",[58,338,167],{"class":75},[58,340,226],{"class":225},[58,342,229],{"class":79},[58,344,345],{"class":225}," 60",[58,347,331],{"class":75},[58,349,350,353,355],{"class":60,"line":217},[58,351,352],{"class":157},"        swr",[58,354,167],{"class":75},[58,356,357],{"class":64},"true\n",[58,359,360],{"class":60,"line":235},[58,361,362],{"class":75},"      }\n",[58,364,366],{"class":60,"line":365},10,[58,367,368],{"class":75},"    }\n",[58,370,372],{"class":60,"line":371},11,[58,373,374],{"class":75},"  }\n",[58,376,378],{"class":60,"line":377},12,[58,379,238],{"class":75},[21,381,382],{},"Now your handler goes back to being a plain, boring event handler:",[49,384,386],{"className":51,"code":385,"language":53,"meta":54,"style":54},"\u002F\u002F server\u002Fapi\u002Ftmdb\u002F[...path].ts\nexport default defineEventHandler(async (event) => {\n  const path = event.context.params.path\n  const config = useRuntimeConfig(event) \u002F\u002F works normally again\n  return $fetch(`https:\u002F\u002Fapi.themoviedb.org\u002F3\u002F${path}`, {\n    headers: { Authorization: `Bearer ${config.tmdbToken}` }\n  })\n})\n",[25,387,388,393,416,443,462,487,515,519],{"__ignoreMap":54},[58,389,390],{"class":60,"line":61},[58,391,392],{"class":273},"\u002F\u002F server\u002Fapi\u002Ftmdb\u002F[...path].ts\n",[58,394,395,397,399,402,404,406,408,410,412,414],{"class":60,"line":99},[58,396,65],{"class":64},[58,398,68],{"class":64},[58,400,401],{"class":71}," defineEventHandler",[58,403,76],{"class":75},[58,405,80],{"class":79},[58,407,83],{"class":75},[58,409,87],{"class":86},[58,411,90],{"class":75},[58,413,93],{"class":75},[58,415,96],{"class":75},[58,417,418,420,423,425,428,430,433,435,438,440],{"class":60,"line":121},[58,419,102],{"class":79},[58,421,422],{"class":86},"path",[58,424,108],{"class":75},[58,426,427],{"class":86}," event",[58,429,180],{"class":75},[58,431,432],{"class":86},"context",[58,434,180],{"class":75},[58,436,437],{"class":86},"params",[58,439,180],{"class":75},[58,441,442],{"class":86},"path\n",[58,444,445,447,449,451,453,455,457,459],{"class":60,"line":154},[58,446,102],{"class":79},[58,448,105],{"class":86},[58,450,108],{"class":75},[58,452,111],{"class":71},[58,454,76],{"class":75},[58,456,87],{"class":86},[58,458,90],{"class":75},[58,460,461],{"class":273}," \u002F\u002F works normally again\n",[58,463,464,466,468,470,472,475,477,479,481,483,485],{"class":60,"line":194},[58,465,203],{"class":64},[58,467,134],{"class":71},[58,469,76],{"class":75},[58,471,140],{"class":139},[58,473,474],{"class":143},"https:\u002F\u002Fapi.themoviedb.org\u002F3\u002F",[58,476,175],{"class":64},[58,478,422],{"class":143},[58,480,186],{"class":64},[58,482,140],{"class":139},[58,484,149],{"class":75},[58,486,96],{"class":75},[58,488,489,491,493,495,497,499,501,503,505,507,509,511,513],{"class":60,"line":200},[58,490,158],{"class":157},[58,492,161],{"class":75},[58,494,164],{"class":157},[58,496,167],{"class":75},[58,498,140],{"class":139},[58,500,172],{"class":143},[58,502,175],{"class":64},[58,504,105],{"class":143},[58,506,180],{"class":75},[58,508,183],{"class":143},[58,510,186],{"class":64},[58,512,140],{"class":139},[58,514,191],{"class":75},[58,516,517],{"class":60,"line":209},[58,518,197],{"class":75},[58,520,521],{"class":60,"line":217},[58,522,238],{"class":75},[21,524,525,526,529,530,533],{},"The ",[25,527,528],{},"swr: true"," flag is the part worth calling out — it gives you stale-while-revalidate for free. The first request after expiry gets a slightly stale response immediately, while Nitro fetches a fresh copy in the background and serves that to the ",[249,531,532],{},"next"," request. Your users almost never see a slow, uncached response, even on a cold cache.",[41,535,537],{"id":536},"why-this-matters-more-on-the-edge","Why this matters more on the edge",[21,539,540,541,544,545,548],{},"If you're deploying to Cloudflare Workers (via the ",[25,542,543],{},"cloudflare_module"," preset), this separation matters even more. Workers have a strict execution model, and KV-backed caching through route rules plays nicely with that model because Nitro handles the storage adapter for you. You're not manually reading and writing to ",[25,546,547],{},"event.context.cloudflare.env.YOUR_KV_NAMESPACE"," in every handler — you configure it once and every matching route gets consistent behavior.",[21,550,551,552,554],{},"There's a bonus here too: because the caching decision lives in config rather than code, you can change your caching strategy for an entire section of your API without touching a single handler. Need to make search results cache for less time than movie details? Two lines in ",[25,553,262],{},", not two different handler implementations.",[41,556,558],{"id":557},"the-general-principle","The general principle",[21,560,561,562,565],{},"This isn't really about TMDB or Cloudflare specifically. It's a broader habit worth building: ",[37,563,564],{},"when a cross-cutting concern — caching, auth, rate limiting — starts showing up inside multiple handlers, that's usually a sign it belongs one layer up."," Handlers should answer \"what does this endpoint do.\" Routing config should answer \"how does this endpoint behave.\" Mixing the two together is how you end up with a handler that's 30% business logic and 70% infrastructure plumbing.",[21,567,568,569,572],{},"Next time you catch yourself writing the same ",[25,570,571],{},"if (cached) return cached"," block for the third time, stop and ask whether your framework already has a declarative way to say what you're trying to do by hand.",[574,575],"hr",{},[21,577,578],{},[249,579,580],{},"This is the first post on this blog — more notes like this will follow as I keep building things and hitting walls worth writing about.",[582,583,584],"style",{},"html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .scnC2, html code.shiki .scnC2{--shiki-default:#B5695977;--shiki-dark:#C98A7D77}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html pre.shiki code .sHLBJ, html code.shiki .sHLBJ{--shiki-default:#998418;--shiki-dark:#B8A965}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}",{"title":54,"searchDepth":99,"depth":99,"links":586},[587,588,589,590],{"id":43,"depth":99,"text":44},{"id":255,"depth":99,"text":256},{"id":536,"depth":99,"text":537},{"id":557,"depth":99,"text":558},"2026-08-22","When I first wired up a TMDB proxy for a Nuxt project running on Cloudflare Workers, my instinct was the same as most developers': write a caching layer by hand. Check a KV key, if it's missing fetch fresh data, write it back, set a TTL, repeat. It works. It also turns into a mess fast — cache invalidation logic scattered across handlers, config values duplicated everywhere, and a defineCachedEventHandler fighting with useRuntimeConfig(event) in ways that are miserable to debug.","md",{},true,"\u002Fblog\u002Fstop-caching-everyhing-by-hand",{"title":12,"description":592},"blog\u002Fstop-caching-everyhing-by-hand",[600,601,602,603],"nuxt","cloudflare","caching","performance","ZmhorujqLHYknLMjHv78XN2_QSUV6Phq7UTy4IwSwCQ"]