<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: eddylee</title>
    <description>The latest articles on Forem by eddylee (@_a9b502091e5f4cba28f13).</description>
    <link>https://forem.com/_a9b502091e5f4cba28f13</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3865783%2F565a111f-076c-45ed-a364-4ec2dd798edf.png</url>
      <title>Forem: eddylee</title>
      <link>https://forem.com/_a9b502091e5f4cba28f13</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/_a9b502091e5f4cba28f13"/>
    <language>en</language>
    <item>
      <title>Filling a maintainer's "Help needed": shipping a Next.js 16 Redis cache handler</title>
      <dc:creator>eddylee</dc:creator>
      <pubDate>Sat, 09 May 2026 21:47:08 +0000</pubDate>
      <link>https://forem.com/_a9b502091e5f4cba28f13/filling-a-maintainers-help-needed-shipping-a-nextjs-16-redis-cache-handler-1dbe</link>
      <guid>https://forem.com/_a9b502091e5f4cba28f13/filling-a-maintainers-help-needed-shipping-a-nextjs-16-redis-cache-handler-1dbe</guid>
      <description>&lt;h1&gt;
  
  
  Filling a maintainer's "Help needed": shipping a Next.js 16 Redis cache handler
&lt;/h1&gt;

&lt;p&gt;Next.js 16 split caching into two distinct handler interfaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cacheHandler&lt;/code&gt;&lt;/strong&gt; (singular) — Pages Router ISR, on-demand revalidation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cacheHandlers&lt;/code&gt;&lt;/strong&gt; (plural) — the new &lt;code&gt;'use cache'&lt;/code&gt; directive, &lt;code&gt;cacheComponents: true&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most popular OSS Redis handler today is &lt;a href="https://github.com/fortedigital/nextjs-cache-handler" rel="noopener noreferrer"&gt;&lt;code&gt;@fortedigital/nextjs-cache-handler@3.2.0&lt;/code&gt;&lt;/a&gt;. It declares &lt;code&gt;peerDependencies.next: "&amp;gt;=16.1.5"&lt;/code&gt;. But its README marks the entire &lt;strong&gt;plural-API column&lt;/strong&gt; as ❌:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cacheHandlers config (plural)         ❌ Not yet supported - Help needed
'use cache' directive                 ❌ Not yet supported - Help needed
'use cache: remote' directive         ❌ Not yet supported - Help needed
'use cache: private' directive        ❌ Not yet supported - Help needed
cacheComponents                       ❌ Not yet supported - Help needed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The community attempt to fix this — &lt;a href="https://github.com/fortedigital/nextjs-cache-handler/pull/207" rel="noopener noreferrer"&gt;PR #207&lt;/a&gt; — has been stalled for three months on a &lt;code&gt;PHASE_PRODUCTION_BUILD&lt;/code&gt; regression that the maintainer rejected. The maintainer also said in &lt;a href="https://github.com/fortedigital/nextjs-cache-handler/issues/152" rel="noopener noreferrer"&gt;Issue #152&lt;/a&gt;: &lt;em&gt;"Next.js does not care about any other cloud or cluster environment than Vercel"&lt;/em&gt; — a candid acknowledgement that fortedigital's roadmap may not include this any time soon.&lt;/p&gt;

&lt;p&gt;I had a multi-instance Next.js 16 deployment running on AWS ECS Fargate that needed all of this working &lt;strong&gt;today&lt;/strong&gt;. So I built a separate small package focused on filling those gaps:&lt;/p&gt;

&lt;p&gt;📦 &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/@leejpsd/nextjs-cache-handler" rel="noopener noreferrer"&gt;&lt;code&gt;@leejpsd/nextjs-cache-handler&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — currently &lt;code&gt;0.2.0&lt;/code&gt;, MIT licensed.&lt;/p&gt;

&lt;p&gt;This post is the technical writeup — what it does, why it exists, the trap that almost shipped silently, and what live-traffic dogfood actually verified.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;p&gt;If you have a Next.js 16 app deployed across multiple containers (ECS task / Kubernetes pod / Fly.io machine), the default in-memory cache fragments per-instance. Two tasks behind one ALB will independently evaluate &lt;code&gt;'use cache'&lt;/code&gt; functions, write into their own local LRU, and never see each other's writes. &lt;code&gt;revalidateTag('posts')&lt;/code&gt; only invalidates the task that received the call.&lt;/p&gt;

&lt;p&gt;The fix Next.js documents is &lt;em&gt;"register a custom cache handler that writes to a shared store"&lt;/em&gt;. The interface is well-defined; the actual implementation has more landmines than the docs imply.&lt;/p&gt;

&lt;p&gt;This package implements &lt;strong&gt;both&lt;/strong&gt; interfaces in one wrapper, with a few production-driven defaults that the upstream OSS landscape currently doesn't cover.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cacheComponents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cacheHandler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./cache-incremental.cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;cacheHandlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./cache-components.cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// cache-components.cjs&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createCacheComponentsHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@leejpsd/nextjs-cache-handler/cache-components&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCacheComponentsHandler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;redis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIS_URL&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;buildNamespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEPLOYMENT_VERSION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// auto deploy isolation&lt;/span&gt;
  &lt;span class="na"&gt;abortTimeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;staleWhileRevalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;singleFlight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// optional, opt-in stampede protection (v0.2)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. &lt;code&gt;'use cache'&lt;/code&gt;, &lt;code&gt;revalidateTag&lt;/code&gt;, &lt;code&gt;updateTag&lt;/code&gt;, &lt;code&gt;cacheLife&lt;/code&gt; all work. The library handles the build-time vs runtime split, the Lua-atomic tag updates, and the deploy-boundary key namespacing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Compatibility matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;this&lt;/th&gt;
&lt;th&gt;@fortedigital 3.2.0&lt;/th&gt;
&lt;th&gt;nextjs-turbo-redis-cache 1.13&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cacheHandlers&lt;/code&gt; config (plural)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ Help needed&lt;/td&gt;
&lt;td&gt;✅ since 1.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;'use cache'&lt;/code&gt; directive&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ Help needed&lt;/td&gt;
&lt;td&gt;✅ since 1.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;'use cache: remote'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cacheComponents: true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ Production-validated&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build-phase skip (&lt;code&gt;PHASE_PRODUCTION_BUILD&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✅ default-on&lt;/td&gt;
&lt;td&gt;✅ (singular only)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto deploy isolation&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;BUILD_NAMESPACE&lt;/code&gt; env-resolved&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;BUILD_ID&lt;/code&gt; since 1.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lua-atomic SET+tag&lt;/td&gt;
&lt;td&gt;✅ Lua scripts&lt;/td&gt;
&lt;td&gt;partial (MULTI)&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-flight refresh lock&lt;/td&gt;
&lt;td&gt;✅ opt-in (v0.2)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AbortSignal timeout&lt;/td&gt;
&lt;td&gt;✅ per-op&lt;/td&gt;
&lt;td&gt;✅ Proxy-wrapped&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenTelemetry hook&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;onMetric&lt;/code&gt; (v0.2)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration tests vs real Redis&lt;/td&gt;
&lt;td&gt;✅ 21 scenarios (v0.2)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live-traffic dogfood report&lt;/td&gt;
&lt;td&gt;✅ public 24h soak&lt;/td&gt;
&lt;td&gt;not published&lt;/td&gt;
&lt;td&gt;not published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(Verified 2026-05-10. Both upstream packages move quickly; please check their READMEs for the latest state.)&lt;/p&gt;




&lt;h2&gt;
  
  
  The trap that almost shipped silently
&lt;/h2&gt;

&lt;p&gt;The most useful artifact in this whole exercise wasn't the handler implementation — it was a single landmine I tripped during dogfood deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup&lt;/strong&gt;: an env-var toggle in &lt;code&gt;next.config.ts&lt;/code&gt; that flips between the in-tree handler (existing implementation) and the new library, so I could ship the library to staging behind a one-flag rollback.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.ts (the buggy version)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useLibrary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;USE_LIBRARY_HANDLER&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useLibrary&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./lib-cache-components.cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./redis-handler.cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cacheHandlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks fine, right? Toggle flag, swap path, done.&lt;/p&gt;

&lt;p&gt;I deployed this. CloudWatch confirmed &lt;code&gt;USE_LIBRARY_HANDLER=true&lt;/code&gt; was set on the ECS task. Cache state inspection showed entries being written. &lt;strong&gt;But the cache key shapes were wrong&lt;/strong&gt; — they had no &lt;code&gt;BUILD_NAMESPACE&lt;/code&gt; prefix, which is the library's signature feature.&lt;/p&gt;

&lt;p&gt;I added &lt;code&gt;console.log("loaded")&lt;/code&gt; to the library wrapper. Re-deployed. Searched CloudWatch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 results.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The library wrapper was never being required at runtime. Despite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;USE_LIBRARY_HANDLER=true&lt;/code&gt; correctly set&lt;/li&gt;
&lt;li&gt;The deploy commit hash showing the latest code&lt;/li&gt;
&lt;li&gt;The library being installed in &lt;code&gt;node_modules&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;next.config.ts&lt;/code&gt; toggle logic being correct&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What actually happened
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;next.config.ts&lt;/code&gt; is evaluated at &lt;strong&gt;build time&lt;/strong&gt;. Specifically, &lt;code&gt;require.resolve(...)&lt;/code&gt; resolves the absolute file path &lt;strong&gt;once during the Docker build&lt;/strong&gt;, then bakes that resolved path into the standalone server bundle.&lt;/p&gt;

&lt;p&gt;In the Docker build environment, &lt;code&gt;USE_LIBRARY_HANDLER&lt;/code&gt; was not set. So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;build time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;process.env.USE_LIBRARY_HANDLER === undefined&lt;/span&gt;
  &lt;span class="s"&gt;→ useLibrary === &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="s"&gt;→ path = "./redis-handler.cjs"&lt;/span&gt;
  &lt;span class="s"&gt;→ require.resolve("./redis-handler.cjs") = "/abs/path/redis-handler.cjs"&lt;/span&gt;
  &lt;span class="s"&gt;→ that absolute path is what Next.js bakes into the server bundle&lt;/span&gt;

&lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;process.env.USE_LIBRARY_HANDLER === "true"  // (irrelevant — already baked)&lt;/span&gt;
  &lt;span class="s"&gt;→ Next.js loads /abs/path/redis-handler.cjs&lt;/span&gt;
  &lt;span class="s"&gt;→ the library is NEVER required&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runtime env var was completely ignored.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: a request-time router module
&lt;/h3&gt;

&lt;p&gt;Move the env check from &lt;code&gt;next.config.ts&lt;/code&gt; into a dedicated module that's loaded at request time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// cache-components-router.cjs&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use strict&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useLibrary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;USE_LIBRARY_HANDLER&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useLibrary&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./lib-cache-components.cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./redis-handler.cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;next.config.ts&lt;/code&gt; always points at the same router file. The router reads the env var when it's actually loaded — which is when a request comes in, with the runtime environment fully populated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.ts (fixed)&lt;/span&gt;
&lt;span class="nx"&gt;cacheHandlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./cache-components-router.cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus an &lt;code&gt;outputFileTracingIncludes&lt;/code&gt; so Next.js's standalone build copies both backend handler files (the library AND the in-tree fallback) into &lt;code&gt;.next/standalone/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;outputFileTracingIncludes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/**/*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./cache-components-router.cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./incremental-router.cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./lib-cache-components.cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./lib-incremental-cache-handler.cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./redis-handler.cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./incremental-cache-handler.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./node_modules/@leejpsd/nextjs-cache-handler/**/*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, the library activated correctly. CloudWatch logs showed the wrapper being loaded. Cache keys carried the &lt;code&gt;BUILD_NAMESPACE&lt;/code&gt; prefix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters
&lt;/h3&gt;

&lt;p&gt;If you're writing a Next.js cache handler — or any module loaded by &lt;code&gt;next.config.ts&lt;/code&gt; — the &lt;strong&gt;build-time vs runtime trap&lt;/strong&gt; is silent in the worst possible way: no errors, no warnings, just the wrong code path at runtime. Even Next.js's own docs don't call it out clearly.&lt;/p&gt;

&lt;p&gt;The full writeup with diagrams is in &lt;a href="https://github.com/leejpsd/nextjs-cache-handler/blob/main/docs/build-phase.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/build-phase.md&lt;/code&gt;&lt;/a&gt;. It's also exactly the trap that the &lt;a href="https://github.com/fortedigital/nextjs-cache-handler/pull/207#pullrequestreview-" rel="noopener noreferrer"&gt;PR #207 maintainer review&lt;/a&gt; was pointing at — and which has now been resolved cleanly in this package's &lt;code&gt;shouldUseRedis()&lt;/code&gt; helper.&lt;/p&gt;




&lt;h2&gt;
  
  
  What live-traffic dogfood verified
&lt;/h2&gt;

&lt;p&gt;Before promoting &lt;code&gt;0.1.0-rc.1&lt;/code&gt; to stable, I deployed it behind the env-var toggle described above and let production traffic flow through it on AWS ECS Fargate (multi-instance, ElastiCache Redis).&lt;/p&gt;

&lt;p&gt;Snapshot from the validation window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl /api/cache-debug | jq &lt;span class="s1"&gt;'.cacheState'&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"entryKeys"&lt;/span&gt;: 2,
  &lt;span class="s2"&gt;"tagKeys"&lt;/span&gt;: 2,
  &lt;span class="s2"&gt;"tagExpirationKeys"&lt;/span&gt;: 1,
  &lt;span class="s2"&gt;"incrementalEntryKeys"&lt;/span&gt;: 9,
  &lt;span class="s2"&gt;"incrementalTagKeys"&lt;/span&gt;: 1,
  &lt;span class="s2"&gt;"sample"&lt;/span&gt;: &lt;span class="s2"&gt;"next-cache:entry:8d5a4f71c4cc:[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;build-...&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;curl /api/health | jq &lt;span class="s1"&gt;'.checks.redis'&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"ok"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;,
  &lt;span class="s2"&gt;"latencyMs"&lt;/span&gt;: 2,
  &lt;span class="s2"&gt;"reason"&lt;/span&gt;: null
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to note:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cache key shapes carry the &lt;code&gt;BUILD_NAMESPACE&lt;/code&gt; prefix&lt;/strong&gt; (&lt;code&gt;8d5a4f71c4cc&lt;/code&gt; is the deployment SHA). If the in-tree handler were active, keys would be &lt;code&gt;next-cache:entry:["build-..."]&lt;/code&gt; with no namespace segment. That single character difference is the deployment-isolation guarantee in action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis ping at 2ms&lt;/strong&gt; — well within the 1500ms &lt;code&gt;abortTimeoutMs&lt;/code&gt; budget. No timeout events recorded during the validation window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The signals were stable enough to promote to &lt;code&gt;0.1.0&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  v0.2: three differentiators
&lt;/h2&gt;

&lt;p&gt;A pre-publish self-audit on &lt;code&gt;0.1.0&lt;/code&gt; flagged three gaps the matrix didn't yet cover. v0.2 closes them — each one fills an area that no other Next.js Redis handler currently has:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Single-flight refresh lock
&lt;/h3&gt;

&lt;p&gt;Stale entries in the SWR window are served instantly. With many instances all crossing the &lt;code&gt;revalidate&lt;/code&gt; boundary at the same moment, each one independently triggers its own background refresh — N parallel re-renders for the same key, all hitting your origin once.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;singleFlight: true&lt;/code&gt; adds an opt-in Redis lock at the SWR boundary. The first instance to acquire it becomes the &lt;strong&gt;leader&lt;/strong&gt; and runs the refresh; the rest become &lt;strong&gt;followers&lt;/strong&gt; and keep serving the same stale entry. Lock acquisition uses a Lua-atomic &lt;code&gt;SETNX&lt;/code&gt;-style script with a 10-second TTL (configurable):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- refresh-tag-lock.lua&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;'EX'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two new &lt;code&gt;MetricEvent&lt;/code&gt; types appear on the &lt;code&gt;onMetric&lt;/code&gt; hook so operators can verify leadership balance across the fleet:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;event&lt;/th&gt;
&lt;th&gt;meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cache.stale.refresh.leader&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;this instance just acquired the lock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cache.stale.refresh.follower&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;another instance holds it; we serve stale&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If lock acquisition fails (Redis hiccup), the handler defaults to the follower path. The stale entry is always served, never dropped. The lock is an optimization, not a correctness primitive.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. OpenTelemetry reference adapter
&lt;/h3&gt;

&lt;p&gt;The library deliberately ships zero observability dependencies — the &lt;code&gt;onMetric(event)&lt;/code&gt; hook gives strictly-typed events you wire to whichever stack you already run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/leejpsd/nextjs-cache-handler/tree/main/examples/opentelemetry" rel="noopener noreferrer"&gt;&lt;code&gt;examples/opentelemetry/&lt;/code&gt;&lt;/a&gt; is a copy-paste reference wrapper that exposes a counter (&lt;code&gt;nextjs_cache.events_total&lt;/code&gt;) and a histogram (&lt;code&gt;nextjs_cache.op_latency_ms&lt;/code&gt;), both with bounded cardinality (no cache keys or tag names emitted as attributes).&lt;/p&gt;

&lt;p&gt;Three suggested dashboards in the example README: hit-rate over time, single-flight leadership distribution, op latency p50/p95/p99.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Integration tests against real Redis
&lt;/h3&gt;

&lt;p&gt;72 unit tests with a &lt;code&gt;MockRedisClient&lt;/code&gt; give fast, hermetic coverage of the spec. They don't catch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;redis@5 / ioredis method shape changes between minor versions&lt;/li&gt;
&lt;li&gt;Lua EVAL/EVALSHA semantics on a real server&lt;/li&gt;
&lt;li&gt;Cursor-based scanIterator chunk behavior (the &lt;code&gt;redis@4 → redis@5&lt;/code&gt; upgrade silently broke scanning in the reference deployment, surfacing only under live traffic)&lt;/li&gt;
&lt;li&gt;TTL/EX behavior under real Redis time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;v0.2 adds 21 integration scenarios that bring up Redis 7 in docker-compose and run the full test grid against &lt;strong&gt;both&lt;/strong&gt; redis@5 and ioredis adapters. Same scenarios, swapped underlying client. CI runs them on every PR via a service container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/ci.yml&lt;/span&gt;
&lt;span class="na"&gt;integration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;6390&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;6379&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run test:integration&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;INTEGRATION_REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://127.0.0.1:6390&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hardest bug they caught was during initial setup: a &lt;code&gt;vitest&lt;/code&gt; &lt;code&gt;transform&lt;/code&gt; hook combined with &lt;code&gt;assetsInclude&lt;/code&gt; was running twice on &lt;code&gt;.lua&lt;/code&gt; files, emitting &lt;code&gt;export default "export default \"...\""&lt;/code&gt;, which Redis rejected with &lt;code&gt;'=' expected near 'default'&lt;/code&gt;. A single &lt;code&gt;load&lt;/code&gt; hook (no transform) fixed it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest limitations (v0.2)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The dogfood window is a starting point, not a completion criterion.&lt;/strong&gt; Memory leaks, timer drift, and edge cases that only surface after extended uptime are not yet covered. Patch releases will accumulate live time as the package ages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis Cluster is implemented but not load-tested at scale.&lt;/strong&gt; The &lt;code&gt;hashTag: true&lt;/code&gt; flag routes multi-key Lua scripts to the same slot, but I haven't run a real-world cluster benchmark. v0.3 milestone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel KV / Upstash adapters&lt;/strong&gt; ship in v0.3. Both work today via the standard &lt;code&gt;redis@5&lt;/code&gt; adapter against their Redis-compatible endpoints, but native adapters with edge-runtime support are scoped for v0.3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provenance attestation&lt;/strong&gt; ships from the GitHub Actions OIDC publish path (now wired up in &lt;code&gt;release.yml&lt;/code&gt;). The first stable was published from a local machine without provenance; v0.2.x tarballs published via the workflow will carry the verified attestation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are spelled out in the README's Roadmap section; the goal is that "what's not yet supported" is as visible as "what is".&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd repeat / what I'd change
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Repeat
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dogfood before promotion.&lt;/strong&gt; Running the rc against my own production traffic surfaced the build-time-vs-runtime trap before it could embarrass me publicly. The dogfood plan (&lt;a href="https://github.com/leejpsd/nextjs-cache-handler/blob/main/docs/staging-dogfood.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/staging-dogfood.md&lt;/code&gt;&lt;/a&gt;) is the single most useful piece of project hygiene I added.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frozen spec snapshot in repo.&lt;/strong&gt; I copied Next.js's official &lt;code&gt;cacheHandlers&lt;/code&gt; spec verbatim into &lt;a href="https://github.com/leejpsd/nextjs-cache-handler/blob/main/docs/next16-spec.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/next16-spec.md&lt;/code&gt;&lt;/a&gt; before writing any handler code. CI prints its sha256 on every run so spec drift gets noticed before it bites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compatibility matrix with timestamp.&lt;/strong&gt; Both upstream packages I compare against are evolving. Putting &lt;em&gt;"verified 2026-05-10"&lt;/em&gt; on the matrix is the difference between an honest snapshot and a future lie.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Change
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Should have started with &lt;code&gt;outputFileTracingIncludes&lt;/code&gt; from day one.&lt;/strong&gt; I burned half a day on the build-phase trap because Next.js's &lt;code&gt;output: standalone&lt;/code&gt; quietly strips files that aren't transitively required by the &lt;em&gt;build-time&lt;/em&gt; code path. If you're building anything that gets loaded via &lt;code&gt;require.resolve()&lt;/code&gt; from &lt;code&gt;next.config.ts&lt;/code&gt;, pin it explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Should have shipped Redis Cluster load test results before claiming "Redis Cluster ✅"&lt;/strong&gt; in the matrix. The current matrix line says &lt;em&gt;"unit-tested, not yet load-tested at scale"&lt;/em&gt; — honest, but only because I caught the gap during the pre-publish self-audit. Future me writes the load test first.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @leejpsd/nextjs-cache-handler redis
&lt;span class="c"&gt;# or&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @leejpsd/nextjs-cache-handler ioredis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wiring is two CommonJS wrapper files plus a &lt;code&gt;next.config.ts&lt;/code&gt; toggle. The full quick-start is in the &lt;a href="https://github.com/leejpsd/nextjs-cache-handler#quick-start" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Most useful entry points if you're considering it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/leejpsd/nextjs-cache-handler#compatibility" rel="noopener noreferrer"&gt;README compatibility matrix&lt;/a&gt; — verify against your environment&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/leejpsd/nextjs-cache-handler/blob/main/docs/build-phase.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/build-phase.md&lt;/code&gt;&lt;/a&gt; — the build-time vs runtime trap deep dive&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/leejpsd/nextjs-cache-handler/blob/main/docs/architecture.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/architecture.md&lt;/code&gt;&lt;/a&gt; — read paths, write paths, single-flight state machine&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/leejpsd/nextjs-cache-handler/blob/main/docs/staging-dogfood.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/staging-dogfood.md&lt;/code&gt;&lt;/a&gt; — the verification checklist before promoting your own dogfood to stable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Issues, PRs, and feedback — especially on Redis Cluster behavior under real load — are all welcome at &lt;a href="https://github.com/leejpsd/nextjs-cache-handler" rel="noopener noreferrer"&gt;&lt;code&gt;github.com/leejpsd/nextjs-cache-handler&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclaimer: I'm not affiliated with &lt;code&gt;@fortedigital&lt;/code&gt;, &lt;code&gt;@neshca&lt;/code&gt;, or &lt;code&gt;nextjs-turbo-redis-cache&lt;/code&gt;. The compatibility matrix is verified against their public READMEs as of 2026-05-10; all three projects move quickly and snapshots can go stale. The maintainers of all three deserve credit — the patterns I built on came directly from reading their source.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>redis</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why I Built a 4,000-Line Agent Skill Instead of Another npm Package</title>
      <dc:creator>eddylee</dc:creator>
      <pubDate>Tue, 07 Apr 2026 11:57:57 +0000</pubDate>
      <link>https://forem.com/_a9b502091e5f4cba28f13/why-i-built-a-4000-line-agent-skill-instead-of-another-npm-package-51la</link>
      <guid>https://forem.com/_a9b502091e5f4cba28f13/why-i-built-a-4000-line-agent-skill-instead-of-another-npm-package-51la</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I use Claude Code (and sometimes Cursor) for frontend work every day. And every day, I fix the same mistakes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AI generates this&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks fine. TypeScript is happy. But &lt;code&gt;res.json()&lt;/code&gt; returns &lt;code&gt;any&lt;/code&gt; at runtime — if the API changes shape, this silently breaks in production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AI also loves this&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three separate pieces of state that can represent impossible combinations. &lt;code&gt;isLoading: true&lt;/code&gt; AND &lt;code&gt;data&lt;/code&gt; present? &lt;code&gt;error&lt;/code&gt; set but &lt;code&gt;isLoading&lt;/code&gt; still true?&lt;/p&gt;

&lt;p&gt;And my personal favorite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// slapped on the page component&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...entire page is now client-rendered&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These aren't obscure edge cases. They happen constantly because AI agents don't have a structured reference for frontend TypeScript patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Fix It Each Time?
&lt;/h2&gt;

&lt;p&gt;I did. For months. Then I realized:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I'm correcting the same patterns over and over&lt;/li&gt;
&lt;li&gt;My corrections aren't persisted between sessions&lt;/li&gt;
&lt;li&gt;Every new conversation starts from zero&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I needed something the agent could &lt;strong&gt;read before generating code&lt;/strong&gt; — not a tutorial I'd paste into chat, but a structured reference it would consult automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/leejpsd/typescript-react-patterns" rel="noopener noreferrer"&gt;typescript-react-patterns&lt;/a&gt;&lt;/strong&gt; — an Agent Skill for Claude Code, Cursor, Codex, and any AI tool that reads &lt;code&gt;SKILL.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;17 files. 4,000+ lines. Three directories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;typescript-react-patterns/
├── SKILL.md              ← Hub: agent rules, decision guide, checklists
├── rules/                ← 11 pattern modules
│   ├── typescript-core.md
│   ├── react-typescript-patterns.md
│   ├── nextjs-typescript.md
│   ├── component-patterns.md
│   ├── data-fetching-and-api-types.md
│   ├── forms-and-validation.md
│   ├── state-management.md
│   ├── performance-and-accessibility.md
│   ├── debugging-checklists.md
│   ├── code-review-rules.md
│   └── anti-patterns.md
└── playbooks/            ← 3 debugging guides
    ├── type-error-debugging.md
    ├── hydration-issues.md
    └── effect-dependency-bugs.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Makes This Different
&lt;/h2&gt;

&lt;p&gt;I've seen a lot of agent skills. Most are collections of code snippets. This one is designed as &lt;strong&gt;decision support&lt;/strong&gt; — helping the agent choose the right pattern, not just showing patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Agent Behavior Rules
&lt;/h3&gt;

&lt;p&gt;The skill starts by telling the agent what to check &lt;em&gt;before&lt;/em&gt; writing any code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this server or client code?&lt;/li&gt;
&lt;li&gt;Is runtime validation needed? (Yes, if data comes from outside the app)&lt;/li&gt;
&lt;li&gt;What Next.js version? (&lt;code&gt;params&lt;/code&gt; is a Promise in 15+)&lt;/li&gt;
&lt;li&gt;What assumptions must NOT be made?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Decision Flowcharts
&lt;/h3&gt;

&lt;p&gt;Not just "here's a pattern" — but "when to use which":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is this data from a server/API?
├─ Yes → TanStack Query (NOT useState)
└─ No → Is it shareable via URL?
   ├─ Yes → searchParams
   └─ No → How many components need it?
      ├─ 1 → useState
      └─ Many → Zustand with selectors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Rule Classification
&lt;/h3&gt;

&lt;p&gt;Every rule is labeled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;[HARD RULE]&lt;/strong&gt; — Violating causes bugs. No exceptions. &lt;em&gt;"Validate API responses at runtime."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[DEFAULT]&lt;/strong&gt; — Recommended unless you have a documented reason. &lt;em&gt;"Use interface for Props."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[SITUATIONAL]&lt;/strong&gt; — Depends on context. &lt;em&gt;"Polymorphic components — only for design-system foundations."&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Before/After That Actually Matter
&lt;/h3&gt;

&lt;p&gt;Not toy examples. Real frontend scenarios:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API typing:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Before&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ After&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;userSchema&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Loading state:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Before — impossible states are representable&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ After — impossible states are unrepresentable&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Debugging Playbooks
&lt;/h3&gt;

&lt;p&gt;When something goes wrong, the agent has step-by-step diagnosis guides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type errors&lt;/strong&gt;: Read bottom-up, classify, check common React/Next.js-specific errors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hydration mismatches&lt;/strong&gt;: Flowchart from symptom to fix (&lt;code&gt;useEffect&lt;/code&gt; vs &lt;code&gt;dynamic&lt;/code&gt; vs &lt;code&gt;Suspense&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;useEffect bugs&lt;/strong&gt;: Infinite loops (unstable deps), stale closures (captured state), missing cleanup&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Code Review Heuristics
&lt;/h3&gt;

&lt;p&gt;The skill distinguishes &lt;strong&gt;risk&lt;/strong&gt; (flag it) from &lt;strong&gt;preference&lt;/strong&gt; (mention it, don't block):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk:&lt;/strong&gt; &lt;code&gt;as&lt;/code&gt; on API data, &lt;code&gt;useEffect&lt;/code&gt; with object deps, server-only import in client component&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preference:&lt;/strong&gt; &lt;code&gt;type&lt;/code&gt; vs &lt;code&gt;interface&lt;/code&gt;, handler naming convention, import ordering&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Use It
&lt;/h2&gt;

&lt;p&gt;One command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/leejpsd/typescript-react-patterns ~/.claude/skills/typescript-react-patterns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent reads &lt;code&gt;SKILL.md&lt;/code&gt; automatically and consults the relevant &lt;code&gt;rules/&lt;/code&gt; file based on context. If you're working on a form, it reads &lt;code&gt;forms-and-validation.md&lt;/code&gt;. If there's a type error, it reads &lt;code&gt;playbooks/type-error-debugging.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Works with Claude Code, Cursor, Codex, Gemini CLI — anything that reads the &lt;code&gt;SKILL.md&lt;/code&gt; format.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Covered
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Module&lt;/th&gt;
&lt;th&gt;Topics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript Core&lt;/td&gt;
&lt;td&gt;Narrowing, generics, utility types, &lt;code&gt;as const&lt;/code&gt;, &lt;code&gt;satisfies&lt;/code&gt;, &lt;code&gt;unknown&lt;/code&gt; vs &lt;code&gt;any&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React Patterns&lt;/td&gt;
&lt;td&gt;Props, children, events, hooks, context, forwardRef, generic components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js&lt;/td&gt;
&lt;td&gt;App Router params, Server Actions, RSC boundaries, Edge, &lt;code&gt;useOptimistic&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component Patterns&lt;/td&gt;
&lt;td&gt;Discriminated Props, compound components, modal/dialog, polymorphic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Fetching&lt;/td&gt;
&lt;td&gt;Zod validation, TanStack Query, &lt;code&gt;Result&amp;lt;T,E&amp;gt;&lt;/code&gt;, pagination, error handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forms&lt;/td&gt;
&lt;td&gt;react-hook-form + Zod, Server Actions, multi-step checkout example&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State Management&lt;/td&gt;
&lt;td&gt;Decision matrix, Zustand (+ middleware), Context, URL state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance &amp;amp; A11y&lt;/td&gt;
&lt;td&gt;Memoization tradeoffs, focus management, &lt;code&gt;aria-live&lt;/code&gt;, keyboard navigation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anti-patterns&lt;/td&gt;
&lt;td&gt;12 mistakes with symptoms, root causes, and fixes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I Learned Building This
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Structure matters more than volume.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Early versions had more files but less structure. The current version has fewer, denser modules with a consistent template: Scope → Key Rules → Examples → Anti-patterns → Review Checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Agent skills need decision support, not just examples.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Showing 10 patterns is less useful than helping the agent &lt;em&gt;choose&lt;/em&gt; between them. Flowcharts and decision matrices are more valuable than code snippets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Classify your rules.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;[HARD RULE] vs [DEFAULT] vs [SITUATIONAL] changed everything. The agent stops treating every guideline as absolute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Cross-references prevent duplication.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every file has &lt;code&gt;See also&lt;/code&gt; links. The agent can navigate between modules without each file repeating everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contributing
&lt;/h2&gt;

&lt;p&gt;The skill is MIT-licensed and PRs are welcome. Priority areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Testing patterns (Vitest, Testing Library)&lt;/li&gt;
&lt;li&gt;Internationalization typing&lt;/li&gt;
&lt;li&gt;More debugging playbooks&lt;/li&gt;
&lt;li&gt;Accessibility deep dive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it and something is wrong or missing, open an issue. This is built to be iterated on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/leejpsd/typescript-react-patterns" rel="noopener noreferrer"&gt;github.com/leejpsd/typescript-react-patterns&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install&lt;/strong&gt;: &lt;code&gt;git clone https://github.com/leejpsd/typescript-react-patterns ~/.claude/skills/typescript-react-patterns&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this is useful, a ⭐ on GitHub helps others find it.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>react</category>
      <category>nextjs</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
