<?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: Meriç Cintosun</title>
    <description>The latest articles on Forem by Meriç Cintosun (@mericcintosun).</description>
    <link>https://forem.com/mericcintosun</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%2F3854042%2F4711d921-320e-44bc-a2a8-8aec225d1770.jpeg</url>
      <title>Forem: Meriç Cintosun</title>
      <link>https://forem.com/mericcintosun</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mericcintosun"/>
    <language>en</language>
    <item>
      <title>Proxy Contracts and Upgradeability Risks: Storage Collision Analysis and Testing Strategies</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Thu, 30 Apr 2026 15:21:20 +0000</pubDate>
      <link>https://forem.com/mericcintosun/proxy-contracts-and-upgradeability-risks-storage-collision-analysis-and-testing-strategies-3ndo</link>
      <guid>https://forem.com/mericcintosun/proxy-contracts-and-upgradeability-risks-storage-collision-analysis-and-testing-strategies-3ndo</guid>
      <description>&lt;p&gt;Immutable code has historically been a feature of blockchain development, not a bug. Once deployed to mainnet, a smart contract cannot be modified. This immutability guarantees that users interact with exactly what they audited, but it also creates a harsh reality: mistakes are permanent, and new functionality cannot be added. The proxy pattern emerged to solve this problem by decoupling logic from state, allowing developers to upgrade contract behavior without losing historical data or breaking user integrations.&lt;/p&gt;

&lt;p&gt;However, proxy patterns introduce their own class of vulnerabilities. Storage layout collisions between proxy contracts and their implementations, version drift across upgrade paths, and subtle state corruption bugs can emerge only after multiple upgrades in production. These risks are not theoretical—they have caused millions in losses across deployed protocols. The difference between a successful upgrade and a catastrophic failure often comes down to whether storage layout was analyzed correctly before deployment and whether the upgrade path was tested systematically in CI/CD.&lt;/p&gt;

&lt;p&gt;This article examines the mechanics of proxy-based upgradeability, identifies where storage collisions occur, and demonstrates how to test upgrade paths before they reach mainnet. We focus on transparent proxy patterns and UUPS (Universal Upgradeable Proxy Standard) implementations, the two most widely deployed approaches in production systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Proxy Architecture and Storage Layout
&lt;/h2&gt;

&lt;p&gt;The proxy pattern works by separating two contracts: a proxy that receives all calls and delegates them to an implementation, and an implementation contract that contains the actual business logic. The proxy maintains its own storage, while the implementation contract provides the code that modifies it. When an upgrade occurs, the proxy's implementation pointer is updated to reference a new implementation contract, but the proxy's storage persists unchanged.&lt;/p&gt;

&lt;p&gt;This separation creates the core design challenge: both the proxy and the implementation need storage, and they must use the same storage layout. The proxy typically stores its admin address and the address of the current implementation contract. The implementation contract stores application state. When the implementation changes, the new implementation must be able to read and write to the proxy's storage exactly as the old implementation did. Any mismatch in slot allocation causes data corruption.&lt;/p&gt;

&lt;p&gt;Consider a simple example. A proxy contract might use storage slot 0 for its admin address. An implementation contract might declare its first state variable at slot 0, expecting that slot to be unoccupied. When the implementation tries to write to its state variable, it overwrites the admin address. The proxy becomes orphaned—no one can call its upgrade function anymore because the admin slot has been corrupted.&lt;/p&gt;

&lt;p&gt;The Solidity compiler allocates storage slots sequentially starting from slot 0. A state variable declared first occupies slot 0, the next occupies slot 1, and so on. Structs and arrays follow specific packing rules. The vulnerability emerges because developers often do not account for the storage that the proxy itself consumes. They write implementations without knowledge of how much storage the proxy needs, or worse, they inherit from OpenZeppelin's Ownable or Upgradeable base classes in both the proxy and the implementation, effectively double-counting storage variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transparent Proxies vs. UUPS: Storage Implications
&lt;/h2&gt;

&lt;p&gt;Transparent proxies use two distinct storage layouts. The proxy contract stores admin and implementation addresses. The implementation contract stores application state. A key mechanism in transparent proxies is the fallback function: if the caller is the admin, the proxy intercepts the call and handles it directly (for upgrade operations). Otherwise, the proxy delegates the call to the implementation. This prevents admin functions from being shadowed by implementation functions with the same signature.&lt;/p&gt;

&lt;p&gt;However, transparent proxies impose storage overhead in the implementation. If the implementation contract is also Ownable, developers sometimes inherit Ownable twice: once from a base contract and once indirectly through the proxy. This creates storage duplication. The implementation's first few slots are now occupied by proxy-related storage that the implementation never actually uses, but the Solidity compiler allocates them anyway.&lt;/p&gt;

&lt;p&gt;UUPS proxies flip the responsibility. The proxy itself is minimal and contains no admin logic. The implementation contract imports a UUPS mixin that provides the upgrade mechanism. The implementation calls a delegatecall to itself to execute the upgrade, avoiding the double-inheritance trap. The trade-off is that the implementation now carries the upgrade responsibility. If a new implementation fails to properly inherit the UUPS mixin, or if a developer accidentally deploys an implementation that does not support upgrades, the contract becomes stuck at that version forever—no further upgrades are possible.&lt;/p&gt;

&lt;p&gt;Storage layout differs between these patterns because UUPS implementations must reserve a specific storage slot for the implementation pointer. The UUPS standard reserves slot &lt;code&gt;uint256(keccak256('eip1967.proxy.implementation')) - 1&lt;/code&gt; to store the implementation address. Any application state that uses this slot, either by accident or through careless variable declaration, will collide with the upgrade mechanism itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnosing Storage Collisions
&lt;/h2&gt;

&lt;p&gt;Storage collisions manifest in several ways. The most obvious is state corruption after an upgrade: the proxy's admin becomes unwritable, or application state variables read stale or incorrect values. The corruption may not be immediate. If the new implementation writes to storage locations that the old implementation never touched, the problem remains dormant until those locations are read.&lt;/p&gt;

&lt;p&gt;To diagnose storage layout, developers must understand exactly which slots each contract uses. The Solidity compiler does not expose this information directly in the bytecode. Instead, developers rely on analysis tools and manual inspection of contract source code.&lt;/p&gt;

&lt;p&gt;OpenZeppelin provides a storage layout report tool that generates a JSON schema documenting every state variable and its corresponding storage slot. Running this tool on both the proxy and the implementation reveals whether their layouts align. The tool integrates into Hardhat and Foundry, the two most common development frameworks.&lt;/p&gt;

&lt;p&gt;Here is how we use the tool with Hardhat. First, install the necessary package:&lt;br&gt;
&lt;/p&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; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; @openzeppelin/hardhat-upgrades
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in a Hardhat script, import the storage layout reporter:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hre&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;hardhat&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getStorageLayout&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;@openzeppelin/hardhat-upgrades/dist/utils&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&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;ProxyContract&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyProxy&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;ImplementationV1&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV1&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;proxyLayout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getStorageLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ProxyContract&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;implLayout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getStorageLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Proxy Layout:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proxyLayout&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Implementation Layout:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;implLayout&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shows each variable's name, type, size in bytes, and slot number. Comparing the two outputs reveals collisions immediately. If the proxy uses slots 0 and 1 for admin and implementation pointers, but the implementation declares its first state variable at slot 0, the collision is obvious.&lt;/p&gt;

&lt;p&gt;Manual inspection also works for smaller contracts. Open the contract source file and count storage manually. Remember that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uint256 variables occupy one slot each.&lt;/li&gt;
&lt;li&gt;Smaller unsigned integers (uint8, uint16, etc.) can be packed together in one slot if they fit.&lt;/li&gt;
&lt;li&gt;Booleans occupy one slot.&lt;/li&gt;
&lt;li&gt;Address variables occupy one slot (20 bytes) plus padding to reach 32 bytes.&lt;/li&gt;
&lt;li&gt;Mappings and dynamic arrays occupy one slot as a storage key and data location.&lt;/li&gt;
&lt;li&gt;Fixed-size arrays of size N occupy N slots if the array element is uint256-sized, or fewer if smaller types pack.&lt;/li&gt;
&lt;li&gt;Structs occupy consecutive slots, with packing applied to struct members.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Practical example: a proxy with variables &lt;code&gt;address admin&lt;/code&gt; and &lt;code&gt;address implementation&lt;/code&gt; occupies two full slots (two addresses do not pack together because each address is 20 bytes and requires 12 bytes of padding). An implementation contract that declares &lt;code&gt;uint256 value&lt;/code&gt; as its first state variable will place that value at slot 2, not slot 0, because slots 0 and 1 are already allocated by the proxy's variables. This layout is correct.&lt;/p&gt;

&lt;p&gt;However, if the implementation declares &lt;code&gt;address myAddress&lt;/code&gt; as its first state variable, the Solidity compiler allocates it to slot 0, colliding with the proxy's admin address. This is incorrect and will cause corruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage Namespacing Patterns
&lt;/h2&gt;

&lt;p&gt;To avoid collisions systematically, developers adopt storage namespacing patterns. The most reliable approach reserves a range of slots for the proxy and another range for the implementation, documenting the boundary clearly.&lt;/p&gt;

&lt;p&gt;The EIP-1967 standard defines storage slot names for common proxy variables. For a transparent proxy, the admin is stored at &lt;code&gt;keccak256("eip1967.proxy.admin") - 1&lt;/code&gt; and the implementation is stored at &lt;code&gt;keccak256("eip1967.proxy.implementation") - 1&lt;/code&gt;. These hashes are so large (well into the 2^256 range) that accidental collisions with application state declared at low slot numbers are virtually impossible.&lt;/p&gt;

&lt;p&gt;Using EIP-1967 slots looks like this in an implementation contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma solidity ^0.8.0;

contract MyImplementation {
    // Reserve the low slots for application state
    uint256 public counter;
    address public owner;

    // The proxy stores its admin and implementation at EIP-1967 slots (very high)
    // No collision risk

    function increment() public {
        counter++;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proxy, in turn, stores its admin and implementation at the EIP-1967 slots, not at slot 0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma solidity ^0.8.0;

contract MyProxy {
    // Not stored at slot 0 or 1, but at EIP-1967 slots
    // This is handled by assembly or a library function

    function _setImplementation(address newImpl) internal {
        bytes32 slot = keccak256("eip1967.proxy.implementation") - 1;
        assembly {
            sstore(slot, newImpl)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By using EIP-1967 slots, the proxy and implementation no longer compete for the low slot numbers. The implementation can declare state variables starting from slot 0 without fear. This pattern has become the standard in production deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Upgrade Paths Before Mainnet
&lt;/h2&gt;

&lt;p&gt;An upgrade path consists of multiple versions. Version 1 is deployed first. Version 2 adds new features and modifies existing state. Version 3 builds on V2, and so on. Testing must verify that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Storage layout is correct for each version transition.&lt;/li&gt;
&lt;li&gt;State is preserved through the upgrade (no corruption or loss).&lt;/li&gt;
&lt;li&gt;New functionality works correctly after the upgrade.&lt;/li&gt;
&lt;li&gt;Old functionality still works after the upgrade.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Comprehensive upgrade path testing requires simulating the entire sequence of upgrades in a test environment before any version reaches mainnet. This means deploying V1, upgrading to V2, verifying state, upgrading to V3, verifying state again, and so on.&lt;/p&gt;

&lt;p&gt;Here is a Hardhat-based test suite that covers an upgrade path with three versions:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expect&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;chai&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;hre&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;hardhat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Upgrade Path: V1 → V2 → V3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;implV1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;implV2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;implV3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &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;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;]&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSigners&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Deploy V1 implementation&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV1&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;implV1&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;ImplementationV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&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;implV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deployed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Deploy proxy pointing to V1&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;Proxy&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyProxy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;implV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&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;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deployed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;should initialize V1 state correctly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&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;proxyAsV1&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&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;proxyAsV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&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;value&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;proxyAsV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCounter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;should upgrade to V2 without state loss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&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;proxyAsV1&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&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;proxyAsV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Deploy V2 implementation&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV2&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;implV2&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;ImplementationV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&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;implV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deployed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Upgrade proxy to V2&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upgradeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;implV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Verify V1 state is preserved&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV2&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&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;value&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;proxyAsV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCounter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;should upgrade from V2 to V3 without state loss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&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;proxyAsV1&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&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;proxyAsV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Upgrade to V2&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV2&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;implV2&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;ImplementationV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&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;implV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deployed&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;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upgradeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;implV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Add V2-specific state&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV2&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&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;proxyAsV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Upgrade to V3&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ImplementationV3&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;implV3&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;ImplementationV3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&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;implV3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deployed&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;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upgradeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;implV3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Verify both V1 and V2 state are preserved&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxyAsV3&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&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;counter&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;proxyAsV3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCounter&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;name&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;proxyAsV3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Test&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;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;should prevent upgrades from non-admin accounts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&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;attacker&lt;/span&gt;&lt;span class="p"&gt;]&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSigners&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;ImplementationV2&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;implV2&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;ImplementationV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&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;implV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deployed&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;proxyAsAttacker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attacker&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proxyAsAttacker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upgradeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;implV2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;be&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revertedWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Caller is not the admin&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;This test suite validates the upgrade path from V1 to V2 to V3. Notice that we perform initialization in V1, upgrade to V2, verify state preservation, add new state in V2, upgrade to V3, and verify that both old and new state persist. This approach catches storage collisions and state corruption bugs.&lt;/p&gt;

&lt;p&gt;Foundry provides similar testing capabilities using Solidity-based tests. Here is the same suite in Foundry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../contracts/MyImplementationV1.sol";
import "../contracts/MyImplementationV2.sol";
import "../contracts/MyImplementationV3.sol";
import "../contracts/MyProxy.sol";

contract UpgradePathTest is Test {
    MyProxy proxy;
    MyImplementationV1 implV1;
    MyImplementationV2 implV2;
    MyImplementationV3 implV3;
    address owner = address(0x1234);

    function setUp() public {
        implV1 = new MyImplementationV1();
        proxy = new MyProxy(address(implV1), owner);
    }

    function testV1StateInitialization() public {
        vm.prank(owner);
        MyImplementationV1(address(proxy)).initialize(100);

        uint256 counter = MyImplementationV1(address(proxy)).getCounter();
        assertEq(counter, 100);
    }

    function testUpgradeToV2PreservesState() public {
        vm.prank(owner);
        MyImplementationV1(address(proxy)).initialize(100);

        implV2 = new MyImplementationV2();

        vm.prank(owner);
        proxy.upgradeTo(address(implV2));

        uint256 counter = MyImplementationV2(address(proxy)).getCounter();
        assertEq(counter, 100);
    }

    function testUpgradeV2ToV3PreservesAllState() public {
        vm.prank(owner);
        MyImplementationV1(address(proxy)).initialize(100);

        implV2 = new MyImplementationV2();
        vm.prank(owner);
        proxy.upgradeTo(address(implV2));

        vm.prank(owner);
        MyImplementationV2(address(proxy)).setName("Test");

        implV3 = new MyImplementationV3();
        vm.prank(owner);
        proxy.upgradeTo(address(implV3));

        uint256 counter = MyImplementationV3(address(proxy)).getCounter();
        string memory name = MyImplementationV3(address(proxy)).getName();

        assertEq(counter, 100);
        assertEq(name, "Test");
    }

    function testNonAdminCannotUpgrade() public {
        implV2 = new MyImplementationV2();

        address attacker = address(0x5678);
        vm.prank(attacker);
        vm.expectRevert("Caller is not the admin");
        proxy.upgradeTo(address(implV2));
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both test suites validate the upgrade path, but Foundry's tests execute within the EVM directly, providing more accurate gas measurements and state simulation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated Storage Layout Validation in CI/CD
&lt;/h2&gt;

&lt;p&gt;Manual testing catches many issues, but automated storage validation in CI/CD catches collisions before human review. Several tools can be integrated into the CI/CD pipeline to check storage layout automatically.&lt;/p&gt;

&lt;p&gt;OpenZeppelin's &lt;code&gt;@openzeppelin/hardhat-upgrades&lt;/code&gt; package includes a validation function that compares storage layouts between versions:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;validateUpgrade&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;@openzeppelin/hardhat-upgrades&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&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;ImplV1Factory&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV1&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;ImplV2Factory&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;hre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContractFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MyImplementationV2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;validateUpgrade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ImplV1Factory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ImplV2Factory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;unsafeAllow&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;constructor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// Allow constructors in upgrade scenarios if needed&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Storage layout validation passed!&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="k"&gt;catch &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="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Storage layout validation failed:&lt;/span&gt;&lt;span class="dl"&gt;"&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;message&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="nf"&gt;exit&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script compares the storage layouts of V1 and V2, checking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Added state variables (allowed).&lt;/li&gt;
&lt;li&gt;Removed state variables (forbidden—causes state shift for remaining variables).&lt;/li&gt;
&lt;li&gt;Reordered state variables (forbidden—causes existing state to be read as wrong types).&lt;/li&gt;
&lt;li&gt;Type changes in existing variables (forbidden in most cases).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Integrating this into GitHub Actions or GitLab CI ensures every pull request that modifies a contract is checked:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Storage Layout Validation&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;contracts/**'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;test/**'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;18'&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 install&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;npx hardhat run scripts/validateStorageLayout.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this workflow runs and storage validation fails, the pull request is blocked from merging until the issue is resolved. This prevents storage collision bugs from reaching production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Storage Mistakes and How to Avoid Them
&lt;/h2&gt;

&lt;p&gt;Several patterns repeatedly cause storage issues in production deployments. Understanding these mistakes helps developers avoid them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Double-inheriting Ownable.&lt;/strong&gt; Many implementations inherit Ownable both directly and indirectly through a base contract. Solidity linearizes inheritance using C3 linearization, but if not carefully managed, a contract can declare the Ownable state variables twice. The second declaration silently overrides the first in the linearization order, causing unexpected storage allocation. To avoid this, inherit Ownable only once, at the top level of the inheritance hierarchy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Forgetting proxy storage overhead.&lt;/strong&gt; Developers often write implementations assuming storage starts at slot 0. If the proxy uses the low slots, the implementation's state ends up at the wrong offsets. The solution is to use EIP-1967 slots for proxy storage, ensuring the proxy and implementation never collide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: Adding variables to base contracts after deployment.&lt;/strong&gt; If a base contract that implementations inherit from is modified to add new state variables, all implementations that use that base will have shifted storage. This is difficult to debug because the change is not in the implementation itself. A best practice is to freeze base contracts after the first production deployment. If new functionality is needed, create a new version of the base contract and migrate implementations to inherit from it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4: Changing array or mapping types.&lt;/strong&gt; A state variable declared as &lt;code&gt;uint256[]&lt;/code&gt; occupies a storage slot as a key. If you later change it to &lt;code&gt;mapping(uint256 =&amp;gt; uint256)&lt;/code&gt;, both constructs use the same storage location, but interpret the data differently. This causes silent corruption. Never change the type of existing state variables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 5: Upgrading without testing the full path.&lt;/strong&gt; Developers sometimes test V1 → V2 but assume V2 → V3 will work automatically. Each upgrade introduces new opportunities for state shift. Test every consecutive upgrade before mainnet deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Example: Upgrading a Token Contract
&lt;/h2&gt;

&lt;p&gt;Consider a token contract that has been deployed for several months. It implements the ERC20 standard and has billions of tokens in circulation. The governance decides to add a fee mechanism: when tokens are transferred, a small percentage goes to a treasury address. A new implementation contract is developed, tested, and deemed ready for upgrade.&lt;/p&gt;

&lt;p&gt;The original V1 implementation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract TokenV1 is ERC20 {
    constructor() ERC20("MyToken", "MTK") {
        _mint(msg.sender, 1000000 * 10 ** 18);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new V2 implementation adds a treasury and fee:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract TokenV2 is ERC20, Ownable {
    address public treasury;
    uint256 public feePercentage;

    function initialize(address _treasury, uint256 _feePercentage) public {
        treasury = _treasury;
        feePercentage = _feePercentage;
    }

    function transfer(address to, uint256 amount) public override returns (bool) {
        uint256 fee = (amount * feePercentage) / 100;
        uint256 amountAfterFee = amount - fee;

        _transfer(msg.sender, to, amountAfterFee);
        _transfer(msg.sender, treasury, fee);

        return true;
    }

    function setFeePercentage(uint256 _feePercentage) public onlyOwner {
        feePercentage = _feePercentage;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the TokenV2 implementation is deployed directly (not behind a proxy), it will have a different storage layout than TokenV1. The ERC20 base contract's internal variables (balances, allowances, totalSupply) occupy the low slots. TokenV2 adds &lt;code&gt;treasury&lt;/code&gt; and &lt;code&gt;feePercentage&lt;/code&gt;, which will be placed at the next available slots. Upgrading from TokenV1 to TokenV2 will preserve ERC20 state but initialize the new treasury and feePercentage to zero (or to whatever values are already in those storage slots from previous contract deployments or noise).&lt;/p&gt;

&lt;p&gt;However, if TokenV2 also inherits from Ownable, and Ownable is not carefully placed in the inheritance hierarchy, the Solidity compiler might place Ownable's owner variable at a storage location that overlaps with one of ERC20's internal variables. This causes the new implementation to read and write to the wrong storage slots.&lt;/p&gt;

&lt;p&gt;To handle this safely, the upgrade process should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy TokenV2 to a testnet and verify storage layout.&lt;/li&gt;
&lt;li&gt;Run storage layout validation to confirm no collisions with TokenV1.&lt;/li&gt;
&lt;li&gt;Initialize a test instance of the proxy with TokenV1 implementation.&lt;/li&gt;
&lt;li&gt;Call ERC20 transfer functions to create state.&lt;/li&gt;
&lt;li&gt;Upgrade to TokenV2.&lt;/li&gt;
&lt;li&gt;Call transfer again and verify balances are correct.&lt;/li&gt;
&lt;li&gt;Call setFeePercentage and verify the fee is applied correctly.&lt;/li&gt;
&lt;li&gt;Only after all tests pass, deploy TokenV2 to mainnet and execute the upgrade.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Storage Layout Tools and Best Practices
&lt;/h2&gt;

&lt;p&gt;Several tools help developers manage storage layout correctly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenZeppelin Hardhat Upgrades&lt;/strong&gt; provides storage inspection and validation. It integrates directly into the Hardhat testing framework and reports detailed storage layouts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Foundry's &lt;code&gt;forge storage&lt;/code&gt; command&lt;/strong&gt; displays the storage layout of a compiled contract. Running &lt;code&gt;forge storage &amp;lt;contract_name&amp;gt;&lt;/code&gt; shows every state variable and its storage slot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slither&lt;/strong&gt;, a static analysis tool from Trail of Bits, can detect some storage-related issues during code review, though it is not specialized for upgrade paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manual inspection&lt;/strong&gt; remains the most reliable method for critical contracts. Taking time to manually count storage slots and document the layout in code comments prevents surprises. Consider adding a storage layout diagram as a comment in the contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma solidity ^0.8.0;

/**
 * MyImplementationV2
 * Storage Layout:
 * Slot 0: uint256 counter (from V1)
 * Slot 1: address owner (from V1)
 * Slot 2: address treasury (new in V2)
 * Slot 3: uint256 feePercentage (new in V2)
 */

contract MyImplementationV2 {
    uint256 public counter;
    address public owner;
    address public treasury;
    uint256 public feePercentage;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This explicit documentation ensures future developers (or your future self) understand the storage layout and respect it during the next upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparing for Mainnet: A Checklist
&lt;/h2&gt;

&lt;p&gt;Before deploying any upgrade to mainnet, verify:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Storage layout has been validated using automated tools.&lt;/li&gt;
&lt;li&gt;The upgrade path has been tested from the current production version through all planned upgrade steps.&lt;/li&gt;
&lt;li&gt;All tests pass in a local fork of mainnet (using Hardhat's &lt;code&gt;--fork&lt;/code&gt; flag or Foundry's &lt;code&gt;--rpc-url&lt;/code&gt; flag).&lt;/li&gt;
&lt;li&gt;No state variables have been removed from previous versions.&lt;/li&gt;
&lt;li&gt;No state variables have been reordered.&lt;/li&gt;
&lt;li&gt;No state variable types have changed.&lt;/li&gt;
&lt;li&gt;The new implementation has been audited if it introduces significant logic changes.&lt;/li&gt;
&lt;li&gt;An upgrade path rollback plan is documented (e.g., how to upgrade to a previous version if the new version is discovered to be broken).&lt;/li&gt;
&lt;li&gt;The admin key is secure and not held by a single person (consider using a multisig or time-lock contract).&lt;/li&gt;
&lt;li&gt;All team members understand the storage layout and upgrade mechanism.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Following this checklist dramatically reduces the risk of storage-related failures in production.&lt;/p&gt;




&lt;p&gt;If you are building Web3 systems that require production-grade documentation or developing a full-stack Next.js application, I am available for both Web3 technical writing and end-to-end web development. Visit my Fiverr profile at &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt; to discuss your project.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>ethereum</category>
      <category>security</category>
      <category>testing</category>
    </item>
    <item>
      <title>Strengthening Protocol Architecture Against Flash Loan Attacks</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Tue, 28 Apr 2026 15:21:21 +0000</pubDate>
      <link>https://forem.com/mericcintosun/strengthening-protocol-architecture-against-flash-loan-attacks-238d</link>
      <guid>https://forem.com/mericcintosun/strengthening-protocol-architecture-against-flash-loan-attacks-238d</guid>
      <description>&lt;p&gt;Flash loan attacks represent one of the most distinctive vulnerabilities in DeFi, exploiting the atomicity of blockchain transactions to borrow and manipulate assets within a single block without putting up collateral. Unlike traditional lending attacks that require sustained capital commitment, flash loans allow attackers to amplify price movements or manipulate oracle feeds by accessing enormous sums of liquidity for exactly 140 milliseconds (the average Ethereum block time). Understanding these attacks requires examining the transaction-level mechanics that enable them, the economic incentives that motivate attackers, and the architectural patterns that protect protocols from exploitation.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Flash Loans Create Attack Surface
&lt;/h2&gt;

&lt;p&gt;Flash loans function as a liquidity primitive. Aave, the protocol that pioneered this feature, made it publicly accessible in 2020. The mechanics are straightforward: a user calls the flash loan function on a lending pool, receives an arbitrary amount of tokens in the same transaction, executes any logic they choose, and must repay the borrowed amount plus a fee (typically 0.05% on Aave) before the transaction completes. If repayment fails, the entire transaction reverts.&lt;/p&gt;

&lt;p&gt;The critical insight is that atomicity creates the vulnerability. Within a single transaction, an attacker borrows a massive amount of an asset, uses it to manipulate a price oracle or drain a liquidity pool, and then repays the loan. Since the transaction either succeeds completely or fails completely, the attacker has zero downside risk if the exploit fails. They only pay the flash loan fee if they successfully extract value from the protocol.&lt;/p&gt;

&lt;p&gt;Consider a concrete scenario: an attacker executes a flash loan attack on a decentralized exchange (DEX) that prices assets based on spot price from a liquidity pool. The attacker borrows a large amount of Token A, sells it immediately on the DEX, crashing the price. They then use this manipulated price to liquidate a user's position or drain collateral from a lending protocol. Finally, they repay the flash loan and keep the difference. The DEX received the sale, its price was moved, and the protocol that relied on that price was exploited, all in a single block.&lt;/p&gt;

&lt;p&gt;The second factor that enables flash loan attacks is oracle dependency. Most DeFi protocols use price feeds from DEXs or chainlink oracles to value assets and manage collateral. When a protocol depends on spot price from a DEX, an attacker with access to temporary capital can move that price. Even protocols that use time-weighted average prices (TWAP) can be vulnerable if the attacker controls enough liquidity to move the TWAP across multiple blocks, or if the protocol uses a short time window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Economic Incentives and Attack Motivation
&lt;/h2&gt;

&lt;p&gt;Flash loan attacks succeed because the economics align perfectly for attackers. The cost is minimal: a single transaction fee and the flash loan fee, typically totaling less than $1000 in Ethereum gas. The upside can be millions of dollars. This asymmetry has attracted sophisticated attackers who use flash loans as one tool in a larger MEV extraction strategy.&lt;/p&gt;

&lt;p&gt;The attacks that generate the highest returns are those that extract value from multiple layers at once. The 2020 bZx attack, which generated approximately $1 million in profit, combined a flash loan with manipulation of a price oracle, forced liquidations, and arbitrage across multiple markets. The attacker didn't just manipulate one price; they orchestrated a cascade of failures across several protocols.&lt;/p&gt;

&lt;p&gt;However, not every flash loan is an attack. Some flash loans are used legitimately for arbitrage: borrowing assets, executing a trade at better prices elsewhere, and repaying the loan. Others are used for atomic refinancing or collateral swaps. The challenge for protocol architects is distinguishing between legitimate flash loan usage and exploitation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attack Vectors Against Common Protocol Architectures
&lt;/h2&gt;

&lt;p&gt;Different types of protocols face different flash loan risks. Lending protocols are vulnerable to oracle manipulation attacks that artificially inflate or deflate collateral values. A protocol where a user has $100 of collateral backing $80 of borrowing normally has a 1.25x collateralization ratio. If an attacker uses a flash loan to crash the collateral price to $50, the user's position becomes under-collateralized and can be liquidated for profit.&lt;/p&gt;

&lt;p&gt;DEXs that use constant product formulas (like Uniswap v2) are inherently vulnerable to spot price manipulation. If a DEX is the primary price feed for a lending protocol, an attacker can drain liquidity or move price with a flash loan, affecting all positions that depend on that price. Uniswap v3, which added concentrated liquidity, introduced additional complexity: an attacker might target a specific price range where liquidity is scarce.&lt;/p&gt;

&lt;p&gt;Protocols that allow uncollateralized borrowing or rely on governance votes are also at risk. If a flash loan can be used to acquire voting power temporarily, an attacker might vote on a malicious proposal that executes in the same block. This attack was used against bZx and other lending protocols where voting power was tied to token holdings.&lt;/p&gt;

&lt;p&gt;The common thread across all flash loan attacks is that they exploit the gap between the atomic world of blockchain transactions and the assumption of lasting state change. Protocols are often built with the assumption that prices move slowly and that attackers cannot accumulate arbitrary capital instantly. Flash loans violate both assumptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defensive Architecture: Price Oracle Design
&lt;/h2&gt;

&lt;p&gt;The first layer of defense is robust price oracle design. Instead of relying on spot prices from a single DEX, protocols should use multiple oracle sources and aggregation mechanisms. Chainlink price feeds, which aggregate prices from multiple sources and use economic incentives to penalize manipulation, are significantly more resistant to flash loan attacks than spot prices from a single liquidity pool.&lt;/p&gt;

&lt;p&gt;When implementing oracle logic, the contract must handle the case where the oracle goes stale or reports an unreasonable price. Many protocols implement a price deviation check: if the current price differs from the previous price by more than a threshold (say, 5%), the transaction reverts. This prevents an attacker from moving the price too far in a single transaction or block.&lt;/p&gt;

&lt;p&gt;Time-weighted average prices (TWAP) add another layer of defense. Instead of using the current price, a protocol can use the average price over the past N blocks. An attacker with a flash loan can move the price in one block, but cannot affect the average over 100 blocks unless they control multiple consecutive blocks. Uniswap v2 made TWAP data available on-chain for exactly this reason.&lt;/p&gt;

&lt;p&gt;However, TWAP is not unbreakable. An attacker who can control several consecutive blocks (through MEV or 51% attack) can still manipulate TWAP prices. Additionally, a protocol that uses a short TWAP window (say, 3 blocks instead of 30) remains vulnerable.&lt;/p&gt;

&lt;p&gt;The most robust approach combines multiple mechanisms: use Chainlink or similar decentralized oracle networks as the primary price feed, implement TWAP as a secondary feed, and add price deviation checks. When all three sources conflict, the protocol should fail conservatively, reverting the transaction rather than proceeding with potentially incorrect prices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defensive Architecture: Transaction Ordering and Rate Limiting
&lt;/h2&gt;

&lt;p&gt;The second layer of defense addresses the mechanics of atomic execution. Some protocols implement guards that prevent a single transaction from moving prices by more than a certain percentage. Uniswap v2 introduced slippage tolerance, which requires that the output of a swap meets a minimum threshold; if the price has moved more than the user specified, the swap reverts.&lt;/p&gt;

&lt;p&gt;However, slippage protection is a user-facing feature, not a protocol-level defense. A protocol that relies entirely on users to set slippage correctly will fail if users are careless or if an attacker controls the user interface.&lt;/p&gt;

&lt;p&gt;Protocol-level rate limiting is more effective. A protocol can track the maximum price movement within a single block or short time window, and reject transactions that exceed it. For example, Uniswap v3 implements a mechanism where concentrated liquidity positions are only created at specific price ticks; large price movements require moving across multiple ticks, which becomes more expensive.&lt;/p&gt;

&lt;p&gt;Some protocols implement timelocks: a state-changing action is not immediately executed, but queued for later execution. For example, a liquidation that affects a user's collateral might require waiting one block before execution. This prevents an attacker from liquidating a position and extracting collateral in a single atomic transaction.&lt;/p&gt;

&lt;p&gt;Timelocks are not a perfect defense against flash loans—an attacker can still move prices within a single block—but they prevent the most rapid attack patterns and allow monitoring systems to detect and respond to anomalous behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defensive Architecture: Collateral and Liquidation Design
&lt;/h2&gt;

&lt;p&gt;The third layer involves how protocols manage collateral and liquidation mechanics. Lending protocols should maintain over-collateralization ratios that account for realistic price movements. If a protocol allows positions with a 1.1x collateralization ratio (meaning $110 of collateral for $100 of borrowing), a 10% price drop triggers liquidation. An attacker with a flash loan can force this scenario.&lt;/p&gt;

&lt;p&gt;Protocols that use longer liquidation windows are more resilient. Instead of allowing instant liquidation, a protocol might queue a liquidation and wait one block before executing it. This delay gives the protocol an opportunity to detect and respond to flash loan attacks, and gives the under-collateralized user a chance to add more collateral.&lt;/p&gt;

&lt;p&gt;Some protocols implement dynamic collateral requirements that tighten during periods of high volatility or high flash loan usage. If the protocol detects that its price oracle has moved significantly in recent blocks, it increases collateral requirements for new positions. This increases the capital required to exploit the protocol via flash loans.&lt;/p&gt;

&lt;p&gt;Liquidation mechanisms should also require that the liquidated collateral be exchanged fairly. Some protocols allow liquidators to purchase collateral at a discount (this is the liquidation incentive). If the discount is too large, an attacker can use a flash loan to create an under-collateralized position, liquidate it themselves at a discount, and extract the difference. Protocols that limit the liquidation discount or require the discount to be earned over time reduce this attack vector.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defensive Architecture: Governance and Access Control
&lt;/h2&gt;

&lt;p&gt;Governance tokens and voting power are another flash loan attack surface. If a protocol allows an attacker to acquire voting power with a flash loan, they can vote on a proposal that benefits them, even if they don't actually hold any tokens on a persistent basis.&lt;/p&gt;

&lt;p&gt;The simplest defense is to use block height snapshots: voting power is measured at the beginning of a block, not at the current block height. When a proposal is created, voting power is recorded at the block before voting began. This prevents flash loans from granting voting power, since the flash loan exists for only one transaction and cannot affect the snapshot from the previous block.&lt;/p&gt;

&lt;p&gt;Some protocols implement a voting delay, requiring that users have held their tokens for a minimum period (say, one block) before they gain voting power. This further limits the time window in which a flash loan can influence governance.&lt;/p&gt;

&lt;p&gt;More sophisticated governance systems use multi-sig delays or timelock mechanisms, where governance actions are not immediately executed but queued for execution after a delay period. This allows other governance participants to notice and potentially veto malicious proposals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Patterns in Technical Documentation
&lt;/h2&gt;

&lt;p&gt;Integrating flash loan defense into technical documentation requires clarity about assumptions and constraints. The documentation should explicitly state which price sources the protocol trusts, and under what conditions prices might be manipulated. For a lending protocol, the documentation should specify the assumed worst-case price movement in a single block, and explain how collateralization ratios were chosen to account for this movement.&lt;/p&gt;

&lt;p&gt;When documenting liquidation mechanisms, the documentation should explain the liquidation incentive structure and the rationale behind it. If the protocol allows a 5% liquidation discount, the documentation should state that this discount compensates liquidators for executing the transaction, and explain why the chosen percentage is resilient to flash loan attacks.&lt;/p&gt;

&lt;p&gt;For governance systems, the documentation should explicitly describe the voting power snapshot mechanism, the voting delay, and the timelock. Developers building on top of the protocol need to understand these constraints to safely integrate the protocol's governance.&lt;/p&gt;

&lt;p&gt;When a protocol's security depends on external price feeds like Chainlink, the documentation must describe how Chainlink prices are accessed, how stale prices are handled, and what happens if Chainlink becomes unavailable. A fallback mechanism should exist, and it should degrade gracefully rather than putting the protocol into an unsafe state.&lt;/p&gt;

&lt;p&gt;The documentation should also include attack scenarios and explain how the protocol's defenses mitigate them. Rather than claiming the protocol is "flash loan resistant," the documentation should describe a specific attack (e.g., "an attacker uses a flash loan to acquire 10% of token supply and vote for a malicious proposal"), explain why this attack fails (e.g., "voting power is measured at block height N-1, before the flash loan was executed"), and quantify the minimum cost and risk to the attacker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Orchestration Rules and Monitoring
&lt;/h2&gt;

&lt;p&gt;Beyond the protocol architecture itself, defending against flash loan attacks requires orchestration rules and monitoring systems. These rules define what constitutes normal behavior and what should trigger alerts or protective actions.&lt;/p&gt;

&lt;p&gt;An orchestration rule might specify: "If a single transaction moves the DEX price by more than 20%, queue all liquidations for the next block and alert the risk management team." This rule codifies the assumption that prices should not move more than a threshold amount in a single transaction, and responds automatically to violations.&lt;/p&gt;

&lt;p&gt;Another rule might specify: "If governance voting power changes by more than 10% in a single block, pause governance proposals until the change is explained." This prevents governance attacks even if the snapshot mechanism has a bug.&lt;/p&gt;

&lt;p&gt;Monitoring systems should track historical price movements, liquidation activity, and flash loan usage. A spike in any of these metrics might indicate an attack in progress. For example, if the protocol normally processes 10 liquidations per day and suddenly processes 100 liquidations in a single block, this is anomalous and should trigger investigation.&lt;/p&gt;

&lt;p&gt;These monitoring rules should be explicitly documented, including the thresholds that trigger alerts and the responses that are taken. This allows developers building on top of the protocol to understand the protocol's expected behavior and to design their own applications accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Case Study: The bZx Attack
&lt;/h2&gt;

&lt;p&gt;The February 2020 bZx attack demonstrates how flash loan vulnerabilities combine with design flaws to create exploitable conditions. The attack had several stages. First, the attacker used a flash loan to borrow a large amount of sUSD from dYdX. Second, they used this capital to dump sUSD on Uniswap, crashing the sUSD price. Third, bZx's oracle reported the crashed price, triggering liquidations of positions that were collateralized in sUSD. Fourth, the attacker liquidated these positions themselves, collecting liquidation fees. Finally, they repaid the flash loan and kept the profit.&lt;/p&gt;

&lt;p&gt;The attack worked because bZx relied on Uniswap spot prices, which were vulnerable to flash loan manipulation. The protocol did not implement TWAP or multiple price sources. Additionally, the liquidation mechanism allowed the attacker to be the liquidator, removing the incentive to liquidate fairly.&lt;/p&gt;

&lt;p&gt;The bZx team responded by implementing multiple price sources and more robust liquidation mechanics. This case is instructive not because it shows an unbeatable attack, but because it shows how flash loan attacks exploit the intersection of several vulnerabilities. No single defense would have prevented the attack, but multiple layers of defense made it possible to block it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Testing and Documentation of Defenses
&lt;/h2&gt;

&lt;p&gt;Documenting flash loan defenses requires including security testing strategies in the technical documentation. The documentation should describe how the protocol verifies its oracle implementation, how it tests liquidation mechanics under stress conditions, and how it validates that governance mechanisms are resilient to flash loan attacks.&lt;/p&gt;

&lt;p&gt;Security testing for oracle defenses should include simulating flash loan attacks where the attacker tries to move prices beyond the protocol's thresholds. The test should verify that the protocol either rejects the attack or handles the price movement safely. The documentation should include the attack scenarios tested and the results.&lt;/p&gt;

&lt;p&gt;For liquidation defenses, testing should simulate scenarios where many positions become under-collateralized simultaneously. The documentation should show that liquidation mechanisms do not create the cascading failures that benefit attackers.&lt;/p&gt;

&lt;p&gt;Governance testing should simulate acquiring voting power via flash loan and attempt to vote on malicious proposals. The documentation should verify that the voting power snapshot mechanism prevents this attack.&lt;/p&gt;

&lt;p&gt;The documentation should also describe the assumptions underlying each defense. For example, if a protocol assumes that a liquidation delay of one block is sufficient to allow monitoring and response, the documentation should explain this assumption and the scenarios under which it holds or fails.&lt;/p&gt;

&lt;p&gt;Professional Web3 documentation requires that these testing strategies and assumptions be explicitly stated, not hidden. Developers need to understand not just what defenses exist, but how they were validated and what edge cases might expose weaknesses.&lt;/p&gt;

&lt;p&gt;If you need professional Web3 documentation for your protocol or a full-stack Next.js application to manage your blockchain infrastructure, visit &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt; to discuss your project requirements.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>blockchain</category>
      <category>ethereum</category>
      <category>security</category>
    </item>
    <item>
      <title>Business Logic Failures in Smart Contracts: SC02:2026 and Mathematical Verification</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Sun, 26 Apr 2026 15:21:23 +0000</pubDate>
      <link>https://forem.com/mericcintosun/business-logic-failures-in-smart-contracts-sc022026-and-mathematical-verification-3p4k</link>
      <guid>https://forem.com/mericcintosun/business-logic-failures-in-smart-contracts-sc022026-and-mathematical-verification-3p4k</guid>
      <description>&lt;p&gt;Smart contract security extends far beyond syntax correctness and gas optimization. A contract can execute without reverting, process transactions according to its code, and still destroy economic value through flawed business logic. These invariant failures represent the most insidious class of vulnerability because they leave no runtime errors, trigger no access control checks, and operate within the written rules of the system.&lt;/p&gt;

&lt;p&gt;The SC02:2026 classification captures a critical gap in development practice: the gap between what code does and what code should do. This distinction separates syntactically valid contracts from economically sound ones. A Solidity contract compiles. It may pass unit tests. It can fail spectacularly in production because the underlying mathematical model that governs token transfers, collateral calculations, or protocol incentives is fundamentally broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Invariants and Their Violations
&lt;/h2&gt;

&lt;p&gt;An invariant is a property that must remain true at all times. In financial protocols, invariants encode the bedrock assumptions of the system: the total supply should never exceed the cap, collateral value should always exceed debt, or user balances should equal the sum of all individual transfers plus minting minus burning. When code execution violates these properties, invariant failure occurs.&lt;/p&gt;

&lt;p&gt;Invariant failures differ from traditional vulnerabilities in their nature and detection. A reentrancy bug triggers an explicit security failure; an invariant failure silently accumulates mathematical debt. The contract executes every function call correctly according to its logic. The ledger updates properly. The balance transfers work. Yet the system drifts into a state where the fundamental economic model collapses, often invisibly, until external systems detect the damage.&lt;/p&gt;

&lt;p&gt;Consider a lending protocol with a simple rule: total borrows must never exceed total deposits times a collateral ratio. This invariant defines the protocol's solvency. A developer implements proper access controls on borrow functions, validates inputs, and updates state consistently. Yet if the deposit function fails to account for a specific token type, or if liquidation logic miscalculates interest accrual, the invariant breaks. Users can borrow more than is mathematically defensible. The protocol becomes insolvent while its code executes flawlessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Categories of Business Logic Failures
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mathematical Model Errors
&lt;/h3&gt;

&lt;p&gt;The most common source of invariant violations stems from mathematical misimplications of the business requirements. A protocol designer wants to distribute rewards proportionally to stake size. The developer implements this using integer arithmetic without understanding truncation effects. After thousands of transactions, the sum of all distributed rewards exceeds the total allocation due to rounding errors introduced at each step. The invariant "total distributed equals allocation" fails quietly.&lt;/p&gt;

&lt;p&gt;Interest calculation presents another classic failure mode. A lending protocol compounds interest per block. The developer multiplies the outstanding balance by an interest rate and adds it to the ledger. Over time, the accumulated interest grows exponentially. But if the implementation fails to handle edge cases, such as blocks with zero elapsed time or deposits made within the same block, the interest calculation breaks. The protocol can overpay or underpay interest in ways that persist through dozens of transactions before detection.&lt;/p&gt;

&lt;p&gt;Price oracle dependencies introduce mathematical brittleness at scale. A protocol relies on external price feeds to determine collateral adequacy. If the oracle updates asynchronously and the smart contract lacks proper staleness checks, liquidation logic operates on stale prices. A collateral price crashes, but the contract still considers it safe because it hasn't seen the new price yet. Users maintain undercollateralized positions until the oracle catches up. The invariant "all positions are adequately collateralized" is violated.&lt;/p&gt;

&lt;h3&gt;
  
  
  State Transition Errors
&lt;/h3&gt;

&lt;p&gt;Business logic failures often emerge from incorrect state transitions rather than incorrect calculations. A token swap protocol maintains a reserve of each asset and expects the product of reserves to never decrease. The constant product formula, xy = k, is the invariant. A developer implements the swap function but forgets to update reserves after token transfers complete. Users receive tokens from the pool, but the pool's recorded reserves remain unchanged. Future swaps operate on an incorrect basis. The invariant collapses as the effective exchange rate drifts further from equilibrium.&lt;/p&gt;

&lt;p&gt;Governance systems are particularly prone to state transition errors. A DAO allows members to vote on proposals and execute them once a threshold is met. The voting state tracks proposal counts, voting power distributed, and execution status. If the implementation forgets to decrement available voting power after a vote is cast, users can vote multiple times. Or if execution fails to mark a proposal as executed, it can be executed again. The invariant "each vote counts exactly once" is violated.&lt;/p&gt;

&lt;p&gt;Upgrade mechanisms introduce subtle state transition risks. A protocol uses a proxy pattern to allow logic upgrades. The invariant states that state variables maintain their meaning across upgrades. But if a new implementation assumes a different variable layout or interprets existing values differently, the state becomes corrupted. A uint256 intended to represent total supply gets reinterpreted as a price oracle address. The invariant "state variables are correctly initialized" fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Access Control and Authorization Failures
&lt;/h3&gt;

&lt;p&gt;Not all invariant violations stem from mathematical errors. Some arise from who is allowed to modify state. A protocol grants minting privileges to an external address with the assumption that it will mint only under specific conditions. If that address is compromised or behaves unexpectedly, the total supply invariant breaks. The contract code is correct; the authorization model is flawed.&lt;/p&gt;

&lt;p&gt;Delegated authority compounds this risk. A contract allows users to delegate their voting power but fails to prevent self-delegation or circular delegation chains. A single user delegates to themselves, and the system double-counts their power. Or a chain of delegations creates an unbounded recursion that exhausts gas. The invariant "voting power is counted exactly once" fails due to delegation logic errors.&lt;/p&gt;

&lt;p&gt;Upgrade authority presents the highest-stakes authorization risk. If a contract owner can upgrade critical logic without timelock or governance review, they can break invariants arbitrarily. A sophisticated protocol might grant upgrade rights to a multisig, but if the multisig is poorly secured or governance is concentrated, the invariant "protocol behavior is decentralized" is violated in practice, even if the code is correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting Invariant Failures
&lt;/h2&gt;

&lt;p&gt;Detecting invariant violations requires explicit verification beyond the tests that verify happy-path behavior. Developers must articulate invariants formally, then prove the code maintains them or identify cases where the code breaks them.&lt;/p&gt;

&lt;p&gt;Formal verification via tools like &lt;strong&gt;Certora&lt;/strong&gt; or &lt;strong&gt;Mythril&lt;/strong&gt; can check invariants statically. These tools translate Solidity into higher-order logic and prove properties across all possible execution paths. A developer specifies that "the sum of all balances equals total supply" and runs the prover. The tool either confirms the invariant always holds or produces a counterexample showing a specific sequence of calls that violates it. This approach catches many mathematical failures but requires expertise to set up correctly and can be expensive to run on large systems.&lt;/p&gt;

&lt;p&gt;Practical invariant testing augments formal verification. Instead of trying to prove properties for all cases, tests exercise the contract across representative scenarios and check invariants at each step. A test mints tokens, transfers them, burns them, and verifies at every stage that total supply equals the sum of balances. Another test exercises liquidation logic and confirms that after liquidation, the system's total collateral always exceeds total borrows times the safety margin.&lt;/p&gt;

&lt;p&gt;Invariant testing frameworks like &lt;strong&gt;Foundry&lt;/strong&gt;'s property-based testing (via differential fuzzing) allow developers to specify invariants once and then generate hundreds of random call sequences to verify the invariant holds. The test runner generates random operations, executes them, checks the invariant, and reports any violation with the sequence of calls that caused it. This approach catches many edge cases that manual tests miss.&lt;/p&gt;

&lt;p&gt;Static analysis of state transitions provides a lighter-weight check. A developer manually traces how each function modifies state and ensures that functions either maintain invariants or explicitly transition from one valid state to another. This is tedious but crucial for critical-path functions like liquidation or token transfers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documenting Invariants for Audit
&lt;/h2&gt;

&lt;p&gt;Professional security audits depend on clear articulation of invariants. An auditor cannot verify something that has never been stated. Many breaches occur because the protocol's invariants exist only in the designer's head, not in code comments, specification documents, or tests.&lt;/p&gt;

&lt;p&gt;A proper audit documentation package includes an explicit invariants section. For each invariant, the documentation specifies what the invariant is, why it matters, which functions are responsible for maintaining it, and where violations would manifest. An example for a lending protocol might read:&lt;/p&gt;

&lt;p&gt;"Invariant 1: Total supply of borrowed tokens must never exceed total supply of lent tokens. This invariant ensures the protocol never owes more assets than it holds. The borrow() function maintains this invariant by checking that requested amount plus existing borrows does not exceed available liquidity. The repay() function decreases the borrowed total. Interest accrual increases the borrowed total via the accrue() function, which must validate that after interest is added, the borrowed total still does not exceed available liquidity."&lt;/p&gt;

&lt;p&gt;This documentation forces the developer to identify exactly which functions touch the invariant and in what ways. An auditor can then focus their review on those specific paths. If a function is missing from the list, that gap is immediately apparent.&lt;/p&gt;

&lt;p&gt;Invariant documentation should also include mathematical proofs for critical invariants. For a constant product market maker, the documentation proves that the product formula xy = k is maintained across all valid swaps. For a lending protocol, it proves that collateral value remains adequate under all liquidation scenarios. These proofs need not use formal notation but must be precise enough that a competent engineer can verify them or identify their flaws.&lt;/p&gt;

&lt;p&gt;The documentation should address edge cases explicitly. If an invariant is "all user balances must be non-negative", the documentation should clarify what happens with zero balances, how rounding affects balance calculations, and whether the invariant applies before or after state updates. This precision prevents misunderstandings that lead to code that maintains a slightly different version of the intended invariant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Patterns That Violate Invariants
&lt;/h2&gt;

&lt;p&gt;Certain coding patterns and design decisions repeatedly lead to invariant failures. Recognizing these patterns allows developers and auditors to focus scrutiny appropriately.&lt;/p&gt;

&lt;p&gt;State updates that happen out of order introduce failures when the intermediate states violate invariants. A swap function receives tokens from the user, calculates the output, and transfers output to the user. If the developer transfers output before recording the receipt of input, an intermediate state exists where the reserves are wrong. If a reentrancy or other unexpected behavior occurs during the token transfer, the invariant is already violated.&lt;/p&gt;

&lt;p&gt;Unchecked external calls provide vectors for invariant violations. A contract calls an external token's transfer function and assumes it either succeeds or reverts. If the token returns false instead of reverting, the contract's state updates as if the transfer succeeded, but the tokens never actually moved. The invariant "balance changes equal token transfers" breaks.&lt;/p&gt;

&lt;p&gt;Approximations and truncation in calculations accumulate over time. A developer uses division to convert units, accepting that some precision is lost. Across thousands of transactions, these small losses compound. Interest calculations, fee collections, and reward distributions are particularly vulnerable. A fee collection function calculates: fee = amount / 100 (treating amount in cents, collecting 1 cent per dollar). After processing 301 cents of transactions, the fee tally shows 3 cents. But the actual fees collected were only 3 cents total, since 1 cent, 1 cent, and 1 cent are lost to truncation. The invariant "fees collected equal amount divided by 100" fails.&lt;/p&gt;

&lt;p&gt;Forgetting to update related state is catastrophic. A transfer function decrements the sender's balance and increments the recipient's balance. But if the function forgets to update a separate "total supply" counter, and that counter is used elsewhere, the invariant "sum of individual balances equals total supply" breaks. This is especially common in protocols that maintain multiple representations of the same logical state.&lt;/p&gt;

&lt;p&gt;Hard-coded values that should be derived introduce brittleness. A protocol hard-codes the interest rate as 5% annually, calculated per block. But if governance changes the rate to 7%, the on-chain calculation is not updated. The invariant "interest accrued matches the current governance-set rate" breaks until code is redeployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Steps for Preventing Invariant Failures
&lt;/h2&gt;

&lt;p&gt;Preventing invariant violations requires discipline throughout the development lifecycle. It begins before code is written.&lt;/p&gt;

&lt;p&gt;First, articulate invariants explicitly before implementation. Write them in plain English, backed by mathematical notation where relevant. For each invariant, identify the state variables it depends on and the functions that must maintain it. Create a single document that serves as a reference for development and audit. This forces clarity and prevents misunderstandings.&lt;/p&gt;

&lt;p&gt;Second, design state management around invariants. If an invariant requires that balances sum to total supply, design the code so that every balance update is a single atomic operation that also updates total supply, or use a computed property that recalculates total supply from balances. Avoid redundant state variables that represent the same information in different ways unless there is a strong reason, and document why.&lt;/p&gt;

&lt;p&gt;Third, write invariant tests that execute alongside unit tests. For each invariant, write a test that verifies it after each major operation. Use property-based testing to generate random sequences of operations and verify invariants hold throughout. Integrate these tests into CI/CD so they run on every commit.&lt;/p&gt;

&lt;p&gt;Fourth, perform a dedicated invariants review during code review. Rather than just checking for syntax errors and logic bugs, reviewers explicitly verify that each function maintains the invariants it should maintain. Create a checklist of invariants and mark which ones each function touches.&lt;/p&gt;

&lt;p&gt;Fifth, use external audits to validate invariant maintenance. Provide the auditors with the invariants documentation and request that their review specifically address whether the code provably maintains each invariant. If an auditor asks "why does this function do X?", and the answer is "to maintain invariant Y", the invariants documentation should already explain that.&lt;/p&gt;

&lt;p&gt;Sixth, consider formal verification for critical invariants. If the protocol manages significant user funds, spending the time and expense to formally verify core invariants is justified. Focus formal verification on the highest-stakes invariants first, such as those guaranteeing solvency or preventing fund loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case Study: A Liquidation Invariant Failure
&lt;/h2&gt;

&lt;p&gt;Consider a hypothetical lending protocol with collateralized borrowing. Users deposit collateral and borrow stablecoins. The protocol's core invariant is: sum of collateral value of all users, times safety margin (e.g., 1.5), must exceed total borrowed stablecoins. This ensures every dollar borrowed is backed by at least 1.5 dollars in collateral.&lt;/p&gt;

&lt;p&gt;The developer implements a liquidation function that an external party can call when a user's collateral falls below the threshold. The liquidation function checks the current collateral value using an oracle, compares it to the user's debt, and if undercollateralized, transfers the collateral to the liquidator and reduces the user's debt.&lt;/p&gt;

&lt;p&gt;However, the oracle price updates asynchronously. Between the time the user's collateral crashes and the oracle updates, the price is stale. A liquidator sees stale data and liquidates the user incorrectly, taking collateral at an outdated price. Or the oracle never updates for a specific asset, and the collateral is considered safe forever even though its real value is zero. The invariant "collateral value times margin exceeds borrows" is violated because the contract's representation of collateral value is wrong.&lt;/p&gt;

&lt;p&gt;To fix this, the developer implements oracle staleness checks. The liquidation function verifies that the price is less than N blocks old before accepting it. For assets with no recent price, the function treats them as zero value, forcing liquidation immediately if a user holds them. Additionally, the developer writes a test that simulates price crashes and verifies that the invariant is maintained across thousands of liquidations.&lt;/p&gt;

&lt;p&gt;The developer also documents the invariant explicitly: "Invariant: totalCollateralValue() * 1.5 &amp;gt;= totalBorrowed. The liquidate() function maintains this by ensuring only users with collateral value less than debt / 1.5 can be liquidated, and liquidation removes sufficient debt to restore the ratio. The getCollateralValue() function must use recent oracle prices, validated by staleness checks. If an oracle is stale for more than N blocks, collateral denominated in that asset is valued at zero."&lt;/p&gt;

&lt;p&gt;An auditor reviews this documentation, sees that the liquidation logic depends entirely on oracle correctness, and focuses their review there. They test oracle edge cases, verify staleness checks, and confirm that the liquidation math is sound.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Role of Auditors in Invariant Verification
&lt;/h2&gt;

&lt;p&gt;External audits serve a critical function in catching invariant violations that development teams miss. But audit effectiveness depends on how clearly teams communicate invariants to auditors.&lt;/p&gt;

&lt;p&gt;An audit that receives no invariants documentation can only review the code for obvious bugs and common vulnerability patterns. It cannot answer the question, "Does this code do what it should?" because the "should" is never specified. The auditor might spend time reviewing a function that is correct given the invariant it maintains, wasting time that could be spent on a different function that actually violates its invariant.&lt;/p&gt;

&lt;p&gt;An audit that receives comprehensive invariants documentation can perform targeted review. The auditor reads the documented invariants, understands the protocol's intended behavior, and then reviews the code to verify it actually maintains those invariants. If a documented invariant is not maintained, the auditor reports it as a critical issue. If the code maintains invariants that are not documented, the auditor questions why, ensuring that undocumented behavior is not accidentally relied upon.&lt;/p&gt;

&lt;p&gt;Auditors should also request formal specifications where feasible. A specification in pseudo-code or structured English describes the intended behavior at a higher level than Solidity. The auditor can then verify that the Solidity implementation correctly translates the specification. Mismatches between specification and implementation reveal invariant violations.&lt;/p&gt;

&lt;p&gt;Additionally, auditors should request data on test coverage and property-based testing results. If the development team has run property-based tests on the contract and verified invariants across thousands of random call sequences, that provides strong evidence that invariants are maintained. If testing is minimal, the audit should be more thorough and more skeptical.&lt;/p&gt;

&lt;p&gt;The most effective audit processes involve regular communication between auditors and developers. As the auditor reviews code and identifies potential invariant violations, the developer can clarify the intent and the auditor can verify whether the concern is real. This iterative process catches issues that a one-way code review might miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment and Monitoring
&lt;/h2&gt;

&lt;p&gt;Even after development and audit, invariant monitoring must continue post-deployment. Many invariant violations only become apparent when the contract operates at scale, under conditions the development team did not anticipate.&lt;/p&gt;

&lt;p&gt;Deployment should include monitoring hooks that check key invariants periodically. An off-chain monitor can call view functions that verify invariants. For a lending protocol, the monitor calls getCollateralValue(), getTotalBorrowed(), and verifies that the invariant holds. If a violation is detected, alerts trigger so the team can investigate.&lt;/p&gt;

&lt;p&gt;Logging should record enough information to reconstruct invariant state at any point in time. Every transaction that affects a critical invariant should log the before and after states of that invariant. Analysis tools can then replay the transaction history and identify precisely when and how an invariant was violated.&lt;/p&gt;

&lt;p&gt;Frontend and indexing layers should compute invariants independently and compare against on-chain state. A subgraph indexing the protocol can calculate the sum of all balances from events and compare it to the total supply recorded on-chain. Discrepancies indicate an invariant violation.&lt;/p&gt;

&lt;p&gt;Governance should include mechanisms to pause critical functionality if an invariant violation is detected. If monitoring detects that collateral value has fallen below required levels, the protocol should be able to pause new borrowing until the situation is resolved. This limits damage from invariant failures.&lt;/p&gt;

&lt;p&gt;Security contact information should be public and monitored. If invariant violations are discovered by third parties, the team needs a rapid way to receive reports and respond. Responsible disclosure processes are essential; developers who discover violations should have a clear path to report them to the team before public disclosure.&lt;/p&gt;




&lt;p&gt;Professional Web3 documentation and full-stack Next.js development require deep technical precision and clear communication with engineering teams. Reach out via &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt; for specialized documentation or application development work.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>security</category>
      <category>softwareengineering</category>
      <category>web3</category>
    </item>
    <item>
      <title>OWASP 2026 Smart Contract Vulnerabilities: Access Control (SC01:2026) Analysis</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Fri, 24 Apr 2026 15:21:20 +0000</pubDate>
      <link>https://forem.com/mericcintosun/owasp-2026-smart-contract-vulnerabilities-access-control-sc012026-analysis-2jen</link>
      <guid>https://forem.com/mericcintosun/owasp-2026-smart-contract-vulnerabilities-access-control-sc012026-analysis-2jen</guid>
      <description>&lt;p&gt;Access control failures represent the highest-severity class of smart contract vulnerabilities in the OWASP 2026 Top 10 list. These vulnerabilities occur when authentication and authorization mechanisms fail to properly restrict what users or roles can do within a contract, leading to unauthorized state changes, fund theft, or protocol manipulation. Unlike traditional web application security where access control failures often affect user privacy, access control failures in smart contracts directly enable theft of assets or permanent protocol compromise.&lt;/p&gt;

&lt;p&gt;The severity stems from smart contracts operating in an adversarial environment where attackers can directly call any public function with any input. Without proper access control, an attacker needs only to identify a sensitive function and call it. The immutability of blockchain means once an exploit succeeds, the damage is permanent and cannot be rolled back by developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Access Control in Smart Contracts
&lt;/h2&gt;

&lt;p&gt;Access control in smart contracts requires three distinct layers: authentication (verifying who is calling), authorization (determining what that caller is permitted to do), and state isolation (ensuring the caller cannot directly manipulate state that should be protected).&lt;/p&gt;

&lt;p&gt;Authentication in smart contracts relies on the sender address, which is cryptographically verified by the blockchain itself. The &lt;code&gt;msg.sender&lt;/code&gt; global variable returns the address of the immediate caller. However, authentication alone proves only who initiated a transaction; it does not establish what they should be permitted to do. Authorization builds on authentication by implementing role-based permissions, time-based restrictions, or function-specific guards.&lt;/p&gt;

&lt;p&gt;State isolation requires that mutable state changes only occur through controlled entry points. Many vulnerabilities arise when developers create administrative functions without protecting them, or when they implement delegation patterns that inadvertently expose control to unintended parties. The core principle is that any function modifying critical state must first verify that &lt;code&gt;msg.sender&lt;/code&gt; holds the required permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Failure Patterns
&lt;/h2&gt;

&lt;p&gt;Analysis of post-2023 DeFi exploits reveals consistent patterns in how access control failures occur. The most common involve unprotected administrative functions, specifically functions that mint tokens, transfer reserves, change protocol parameters, or pause the system. In the Poly Network exploit (August 2021), attackers modified keeper addresses because the contract allowed any caller to execute administrative state changes without verification. While this attack predates the 2026 OWASP classification, it demonstrates the pattern that persists in modern contracts.&lt;/p&gt;

&lt;p&gt;A second pattern involves delegation vulnerabilities where proxies or delegatecall patterns expose administrative functionality to unintended callers. The Wormhole incident (January 2022) involved an uninitialized proxy contract where any caller could claim admin status. The vulnerability existed because initialization logic used delegatecall without protecting the target contract, allowing attackers to assume control. This represents a cross-layer access control failure where the proxy infrastructure failed to enforce authorization.&lt;/p&gt;

&lt;p&gt;A third pattern emerges in protocols that implement role-based access control but incorrectly assign or transfer roles. The BadgerDAO incident (December 2021) exploited approval functions by compromising private keys of addresses holding elevated permissions. While the root cause was key compromise rather than contract logic, the impact was possible only because a single compromised key granted unlimited authority. Lack of multi-signature requirements or time-lock mechanisms for sensitive operations increased the blast radius.&lt;/p&gt;

&lt;p&gt;The 2024 Liquid Staking Security Report identified fourteen distinct staking protocols with access control vulnerabilities, ranging from unprotected withdrawal functions to improper role validation in reward distribution. These vulnerabilities persisted in live protocols because developers had not implemented comprehensive authorization checks across all state-changing functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing Role-Based Authorization
&lt;/h2&gt;

&lt;p&gt;Implementing role-based authorization requires explicit design during the risk modeling phase, before writing any code. The first step is to enumerate all sensitive operations in the protocol: parameter changes, fund transfers, role assignments, and emergency controls. Each operation requires an associated permission level.&lt;/p&gt;

&lt;p&gt;Define roles as semantic categories rather than boolean flags. Instead of a single &lt;code&gt;isAdmin&lt;/code&gt; boolean, create roles such as &lt;code&gt;GOVERNANCE_ROLE&lt;/code&gt;, &lt;code&gt;LIQUIDATOR_ROLE&lt;/code&gt;, &lt;code&gt;PAUSER_ROLE&lt;/code&gt;, and &lt;code&gt;OPERATOR_ROLE&lt;/code&gt;. Each role should encapsulate a specific set of capabilities. This design prevents a single compromise from granting universal access and enables fine-grained revocation.&lt;/p&gt;

&lt;p&gt;The Open Zeppelin AccessControl library implements role-based access using a mapping structure where each role is a bytes32 identifier, and the contract tracks which addresses hold each role. The implementation pattern checks that &lt;code&gt;hasRole(ROLE, msg.sender)&lt;/code&gt; returns true before executing sensitive code. This approach scales efficiently and allows for role-based administration.&lt;/p&gt;

&lt;p&gt;Here is how a basic role structure looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import "@openzeppelin/contracts/access/AccessControl.sol";

contract StakingPool is AccessControl {
    bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE");
    bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    uint256 public rewardRate;
    bool public paused;

    constructor() {
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setupRole(GOVERNANCE_ROLE, msg.sender);
    }

    function setRewardRate(uint256 newRate) external onlyRole(GOVERNANCE_ROLE) {
        rewardRate = newRate;
    }

    function pauseStaking() external onlyRole(PAUSER_ROLE) {
        paused = true;
    }

    function delegateOperatorRole(address newOperator) external onlyRole(GOVERNANCE_ROLE) {
        grantRole(OPERATOR_ROLE, newOperator);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key difference from a simpler owner-based pattern is that this design allows governance to delegate specific capabilities to multiple addresses without granting full control. The &lt;code&gt;DEFAULT_ADMIN_ROLE&lt;/code&gt; acts as the top-level role that can manage all other roles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access Control in Proxy Patterns
&lt;/h2&gt;

&lt;p&gt;Proxy contracts introduce a critical access control dimension because the proxy contract itself controls which implementation contract receives delegatecalls. If the proxy initialization is unprotected, an attacker can claim admin status or redirect delegatecalls to a malicious implementation.&lt;/p&gt;

&lt;p&gt;The UUPS (Universal Upgradeable Proxy Standard) pattern requires the implementation contract to define the upgrade authority, preventing a proxy from being exploited independently. The pattern enforces that only the designated authority can call &lt;code&gt;upgradeTo&lt;/code&gt;, and this check occurs in the implementation contract, not the proxy.&lt;/p&gt;

&lt;p&gt;Here is the authorization check in a UUPS implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract StakingImplementation is UUPSUpgradeable, OwnableUpgradeable {
    uint256 public rewardRate;

    function initialize(address owner) public initializer {
        __Ownable_init();
        _transferOwnership(owner);
    }

    function setRewardRate(uint256 newRate) external onlyOwner {
        rewardRate = newRate;
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;_authorizeUpgrade&lt;/code&gt; function implements the authorization check. This function is called by the proxy before executing any upgrade, and it reverts if &lt;code&gt;msg.sender&lt;/code&gt; is not the owner. Without this override, the proxy cannot enforce authorization at all.&lt;/p&gt;

&lt;p&gt;Transparent proxies separate the admin interface from the implementation interface, ensuring that administrative functions called through the proxy never reach the implementation. This adds complexity but provides stronger isolation. The trade-off is that transparent proxies require more careful testing to verify that function selectors do not collide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time-Locks and Multi-Signature Controls
&lt;/h2&gt;

&lt;p&gt;For critical operations, a single authorized address creates a single point of failure. Time-locks introduce a delay between when an operation is initiated and when it executes, allowing the community or security teams to detect and potentially block malicious proposals. Multi-signature schemes require multiple independent parties to approve an action, distributing the trust requirement.&lt;/p&gt;

&lt;p&gt;The pattern combines both mechanisms: a governance function initiates a proposal, which enters a time-locked queue. After a delay period (typically 48 to 72 hours in production protocols), any caller can execute the proposal if no veto has occurred.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contract TimeLockController {
    bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE");
    bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");

    uint256 public minDelay = 2 days;
    mapping(bytes32 =&amp;gt; bool) public isOperationReady;
    mapping(bytes32 =&amp;gt; uint256) public operationTimestamp;

    function schedule(
        address target,
        uint256 value,
        bytes calldata data,
        bytes32 predecessor,
        bytes32 salt,
        uint256 delay
    ) external onlyRole(PROPOSER_ROLE) {
        bytes32 id = hashOperation(target, value, data, predecessor, salt);
        require(!isOperationReady[id], "Operation already scheduled");
        require(delay &amp;gt;= minDelay, "Delay too short");

        operationTimestamp[id] = block.timestamp + delay;
    }

    function execute(
        address target,
        uint256 value,
        bytes calldata data,
        bytes32 predecessor,
        bytes32 salt
    ) external payable onlyRole(EXECUTOR_ROLE) {
        bytes32 id = hashOperation(target, value, data, predecessor, salt);
        require(block.timestamp &amp;gt;= operationTimestamp[id], "Operation not ready");

        isOperationReady[id] = true;
        (bool success, ) = target.call{value: value}(data);
        require(success, "Execution failed");
    }

    function hashOperation(
        address target,
        uint256 value,
        bytes calldata data,
        bytes32 predecessor,
        bytes32 salt
    ) public pure returns (bytes32) {
        return keccak256(abi.encode(target, value, data, predecessor, salt));
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When integrated with a multi-signature wallet, the proposer role is assigned to the multi-signature contract. The multi-signature contract requires N of M signers to approve before it calls &lt;code&gt;schedule&lt;/code&gt;. This layered approach ensures that at least two separate authorization mechanisms must fail before an attacker gains control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access Control in Token Contracts
&lt;/h2&gt;

&lt;p&gt;ERC-20 tokens and their variants require careful authorization because they directly control asset transfer. The standard pattern protects the &lt;code&gt;mint&lt;/code&gt; and &lt;code&gt;burn&lt;/code&gt; functions with role checks, and the &lt;code&gt;approve&lt;/code&gt; function grants allowance only to the caller themselves.&lt;/p&gt;

&lt;p&gt;The vulnerability arises when developers create token contracts that allow unrestricted minting or burning. The OnePlus incident (a hypothetical case in security research) would involve a token contract where the &lt;code&gt;mint&lt;/code&gt; function lacks the &lt;code&gt;onlyMinter&lt;/code&gt; guard, allowing any caller to create tokens indefinitely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

contract GovernanceToken is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

    constructor() ERC20("Gov Token", "GOV") {
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setupRole(MINTER_ROLE, msg.sender);
    }

    function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    function burn(uint256 amount) external onlyRole(BURNER_ROLE) {
        _burn(msg.sender, amount);
    }

    function burnFrom(address account, uint256 amount) external onlyRole(BURNER_ROLE) {
        uint256 currentAllowance = allowance(account, msg.sender);
        require(currentAllowance &amp;gt;= amount, "Insufficient allowance");
        _approve(account, msg.sender, currentAllowance - amount);
        _burn(account, amount);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;burnFrom&lt;/code&gt; function requires both the &lt;code&gt;BURNER_ROLE&lt;/code&gt; and existing allowance from the account being burned. This prevents a burner from destroying arbitrary user tokens; it can only burn tokens that the user explicitly allowed it to burn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Access Control
&lt;/h2&gt;

&lt;p&gt;Comprehensive testing must verify that protected functions revert when called by unpermitted addresses and succeed when called by permitted ones. Test coverage should include boundary cases: calling with the owner address, calling with an unauthorized address, calling with address zero, and calling with a contract that receives a permission grant but is not properly initialized.&lt;/p&gt;

&lt;p&gt;Here is a test pattern that validates access control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import "forge-std/Test.sol";
import "../src/StakingPool.sol";

contract StakingPoolTest is Test {
    StakingPool pool;
    address governance = address(0x1);
    address operator = address(0x2);
    address unauthorized = address(0x3);

    function setUp() public {
        vm.startPrank(governance);
        pool = new StakingPool();
        pool.grantRole(pool.OPERATOR_ROLE(), operator);
        vm.stopPrank();
    }

    function testGovernanceCanSetRewardRate() public {
        vm.prank(governance);
        pool.setRewardRate(100);
        assertEq(pool.rewardRate(), 100);
    }

    function testOperatorCannotSetRewardRate() public {
        vm.prank(operator);
        vm.expectRevert();
        pool.setRewardRate(100);
    }

    function testUnauthorizedCannotSetRewardRate() public {
        vm.prank(unauthorized);
        vm.expectRevert();
        pool.setRewardRate(100);
    }

    function testGovernanceCanDelegateOperatorRole() public {
        address newOperator = address(0x4);
        vm.prank(governance);
        pool.grantRole(pool.OPERATOR_ROLE(), newOperator);

        vm.prank(newOperator);
        bool hasRole = pool.hasRole(pool.OPERATOR_ROLE(), newOperator);
        assertTrue(hasRole);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each test function validates a single authorization scenario. The test suite should also verify that role revocation works correctly and that the default admin cannot be accidentally removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Implementation Mistakes
&lt;/h2&gt;

&lt;p&gt;The most frequent implementation error is forgetting access control entirely on a newly created function. When developers add functionality during an upgrade or emergency response, they often write the function logic without wrapping it in an authorization check. Code review processes must specifically look for any state-changing function that lacks a require statement or modifier checking permissions.&lt;/p&gt;

&lt;p&gt;A second mistake involves checking &lt;code&gt;msg.sender&lt;/code&gt; against a stored address rather than using role-based patterns. Code like &lt;code&gt;require(msg.sender == admin, "Not admin")&lt;/code&gt; cannot scale and prevents delegation of specific capabilities. Role-based patterns using AccessControl or similar libraries are more composable and maintainable.&lt;/p&gt;

&lt;p&gt;A third mistake occurs when developers implement custom authorization logic instead of using established patterns. Custom logic is difficult to audit and often contains subtle flaws. For example, checking role membership using a boolean mapping is weaker than using OpenZeppelin's AccessControl because it does not support hierarchical roles or efficient revocation.&lt;/p&gt;

&lt;p&gt;A fourth mistake involves not protecting initialization functions in upgradeable contracts. If an initializer can be called by any address, an attacker can reinitialize the contract with different settings. The &lt;code&gt;initializer&lt;/code&gt; modifier from OpenZeppelin Upgradeable Contracts prevents this by ensuring that &lt;code&gt;initialize&lt;/code&gt; executes only once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access Control and Front-Running
&lt;/h2&gt;

&lt;p&gt;Access control mechanisms do not protect against front-running, where an attacker observes a pending transaction and submits a competing transaction that executes first. If a protocol uses access control to limit who can execute liquidations or arbitrage opportunities, a front-runner can include a permissionless function that grants themselves the required role immediately before the liquidation function executes.&lt;/p&gt;

&lt;p&gt;The solution requires separating authorization from execution. Instead of checking roles directly in the liquidation function, the liquidation can accept a signed authorization from an authorized address. The function then verifies the signature before proceeding. This prevents a front-runner from claiming a role they do not actually hold.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contract FrontRunProtectedLiquidation {
    bytes32 public LIQUIDATION_TYPEHASH = 
        keccak256("Liquidation(address borrower,uint256 repayAmount,uint256 nonce)");

    address public authorityAddress;
    mapping(address =&amp;gt; uint256) public nonces;

    function liquidateWithSignature(
        address borrower,
        uint256 repayAmount,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
        bytes32 digest = keccak256(abi.encodePacked(
            "\x19Ethereum Signed Message:\n32",
            keccak256(abi.encode(LIQUIDATION_TYPEHASH, borrower, repayAmount, nonces[borrower]))
        ));

        address recovered = ecrecover(digest, v, r, s);
        require(recovered == authorityAddress, "Invalid signature");

        nonces[borrower]++;
        _executeLiquidation(borrower, repayAmount);
    }

    function _executeLiquidation(address borrower, uint256 amount) internal {
        // Liquidation logic
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern ensures that only a pre-authorized signer can initiate liquidations, and the signature includes a nonce to prevent replay attacks. The front-runner cannot forge a valid signature without the private key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit Checklist for Access Control
&lt;/h2&gt;

&lt;p&gt;When reviewing a smart contract for access control vulnerabilities, validate the following items:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Every state-changing function has an authorization check. This includes mint, burn, transfer from authority, parameter changes, and emergency functions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authorization checks occur before state modifications. If a function modifies state and then checks authorization, the function is vulnerable to re-entrancy or partial execution attacks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Role definitions are semantic and specific. Using multiple small roles (like MINTER_ROLE and BURNER_ROLE) rather than a single ADMIN_ROLE prevents over-privileged addresses.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Role assignment uses established patterns like OpenZeppelin AccessControl or role managers, not custom boolean mappings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Administrative functions like grantRole and revokeRole themselves have authorization checks to prevent unauthorized role changes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Proxy initialization is protected, either through the initializer modifier or by having the proxy deploy with a pre-set admin address that cannot be changed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Time-locks or multi-signature approvals protect critical parameter changes and upgrades.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Signature-based authorization includes replay protection through nonces or domain separators.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test coverage includes both positive cases (authorized caller succeeds) and negative cases (unauthorized caller reverts).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Access control is documented in comments explaining which roles have which capabilities and why those capabilities are necessary.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The development and deployment of secure smart contracts depends on implementing access control during the design phase, not as an afterthought. Developers who define roles, authorization requirements, and delegation patterns before writing code produce contracts that withstand adversarial scrutiny. The OWASP 2026 SC01 classification reflects the reality that access control failures remain the highest-impact vulnerability class in production deployments, and their prevention requires discipline and established patterns.&lt;/p&gt;

&lt;p&gt;I am available for professional Web3 documentation and smart contract analysis, as well as full-stack Next.js development work. You can find my availability at &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>cybersecurity</category>
      <category>security</category>
      <category>web3</category>
    </item>
    <item>
      <title>Sui Move Fundamentals for Developers: Object-Oriented Blockchain Architecture</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Wed, 22 Apr 2026 15:21:20 +0000</pubDate>
      <link>https://forem.com/mericcintosun/sui-move-fundamentals-for-developers-object-oriented-blockchain-architecture-48ho</link>
      <guid>https://forem.com/mericcintosun/sui-move-fundamentals-for-developers-object-oriented-blockchain-architecture-48ho</guid>
      <description>&lt;h2&gt;
  
  
  Understanding Sui's Object Model
&lt;/h2&gt;

&lt;p&gt;Ethereum pioneered the account-based model, where smart contracts maintain global state tied to addresses, and all state transitions flow through centralized account abstractions. This architecture has served the ecosystem well, but it forces a mental model where "everything is a balance" or "everything is a mapping." Sui departs fundamentally from this approach by adopting an object-centric paradigm where blockchain state consists of discrete, independently-owned objects rather than accounts holding collections of data.&lt;/p&gt;

&lt;p&gt;In Sui, objects are first-class citizens. Each object has an immutable ID, a type, an owner (which can be an address, another object, or shared), and version metadata. When you transfer an object, you are not updating a central ledger; you are changing which address holds the reference to that object. This distinction matters because it enables parallel execution of transactions that touch different objects, whereas account-based systems must serialize all transactions that involve the same account.&lt;/p&gt;

&lt;p&gt;The object model also changes how you think about data access control. In Ethereum, a smart contract controls access to data through function modifiers and permission checks. In Sui, ownership is enforced by the blockchain itself. If you own an object, you can include it as a mutable input to a transaction. If you do not own it, the network rejects the transaction at the VM level, before any code executes. This shift moves security enforcement from the contract layer to the consensus layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Move Language: Linearity and Type Safety
&lt;/h2&gt;

&lt;p&gt;Move is the language that powers Sui's smart contracts, but Move originated at Facebook (now Meta) as a language designed around linear types and resource semantics. Unlike Solidity, where a variable can be copied, referenced, or modified freely, Move enforces strict rules about how values move through code. Every value must be used exactly once or explicitly ignored. This is not a style choice; it is enforced by the compiler.&lt;/p&gt;

&lt;p&gt;To understand Move's power, consider a common vulnerability in Solidity: double-spending within the same transaction. A Solidity contract might check a user's balance, transfer tokens to them, and then later in the same function use that balance again because the state update happens asynchronously. Move prevents this at compile time. Once you pass a value to a function or bind it to a new variable, you can no longer use the old binding. The compiler rejects the code before it ever runs.&lt;/p&gt;

&lt;p&gt;Move's type system distinguishes between three categories of values: copy types, drop types, and resource types. Copy types (like booleans and integers) can be duplicated implicitly. Drop types can be discarded implicitly. Resource types must be explicitly handled; they cannot be copied or dropped. When you define a struct as a resource in Move, the compiler ensures that every instance is accounted for. You cannot accidentally lose a resource or create copies without explicit permission. This is why Move is often called a "resources first" language.&lt;/p&gt;

&lt;p&gt;The linear type system has profound implications for security. Integer overflows, a plague in earlier smart contracts, are caught by Move's type checker in safe mode. Reentrancy exploits become structurally impossible because once you pass mutable access to a value into a function, you lose the ability to access it elsewhere in your current call stack. Notional problems like "what if this contract balance increases unexpectedly during a call" are eliminated at the language level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Objects and Abilities in Move
&lt;/h2&gt;

&lt;p&gt;Move objects are structs that can carry capabilities, called abilities. These abilities determine what the compiler allows you to do with instances of that struct. The four abilities are copy, drop, store, and key. A struct with the copy ability can be silently duplicated. A struct with drop can be silently discarded. The store ability determines whether a struct can be stored as a field inside another struct. The key ability signals that this struct is an on-chain object that can be owned and transferred.&lt;/p&gt;

&lt;p&gt;Most on-chain objects in Sui are defined with the key ability and lack copy. This forces the compiler to track unique instances. When you create a resource struct, you are creating something that the blockchain will monitor. Only one address can hold it at a time. You cannot replicate it through normal assignment. If you want to transfer it, you must explicitly call a function that changes its ownership.&lt;/p&gt;

&lt;p&gt;Consider a simple example: defining an asset that represents a claim or a digital item.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module example::asset {
    use sui::object::{Self, UID};
    use sui::tx_context::{Self, TxContext};

    struct Asset has key {
        id: UID,
        value: u64,
    }

    public fun create_asset(value: u64, ctx: &amp;amp;mut TxContext) -&amp;gt; Asset {
        Asset {
            id: object::new(ctx),
            value,
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This struct has the key ability, which means instances live on the blockchain and have identity. The &lt;code&gt;id&lt;/code&gt; field is required; every key struct must have a &lt;code&gt;UID&lt;/code&gt; field named &lt;code&gt;id&lt;/code&gt;. Without the copy ability, passing this struct into a function transfers ownership rather than duplicating it. If a function consumes the Asset (takes it by value), the caller loses access to that instance.&lt;/p&gt;

&lt;p&gt;The ability system is more expressive than it might first appear. It directly maps to what the Move type system can enforce. By removing the copy ability from an Asset struct, we tell the compiler: "every instance of this struct matters, track it carefully." By including the key ability, we tell the blockchain: "this is something that someone owns, index it and make it transferable."&lt;/p&gt;

&lt;h2&gt;
  
  
  Ownership and Access Control
&lt;/h2&gt;

&lt;p&gt;Ownership in Sui is literal and enforced by the protocol. When a transaction creates an object, that object becomes owned by the transaction sender or by an address specified in the transaction. Ownership is not a convention that the contract checks; it is a property that the network verifies. If you are not the owner of an object, you cannot include it as a mutable input to any transaction.&lt;/p&gt;

&lt;p&gt;This creates a radically different access control model than Ethereum. In Ethereum, you call a function and the function checks permission. In Sui, you construct a transaction, and if you lack the necessary ownership, the transaction fails before execution. The blockchain rejects it at the networking layer, before consensus even considers it.&lt;/p&gt;

&lt;p&gt;Shared objects are Sui's mechanism for public state. When you make an object shared, any address can include it in a transaction. Shared objects require consensus to coordinate access, so transactions affecting shared objects may be serialized. Owned objects, by contrast, can be modified in parallel because ownership prevents conflicts. This architectural choice means developers must think about which data should be shared (requiring consensus coordination) and which should be owned (enabling parallel execution).&lt;/p&gt;

&lt;p&gt;An owned object can also be transferred to another address or wrapped inside another object. Wrapped objects are nested within their parent and cannot be accessed directly; any operation on them must go through the parent. This nesting captures complex ownership hierarchies and permission patterns without requiring runtime checks in contract code. The type system enforces it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transactions and Inputs in Sui
&lt;/h2&gt;

&lt;p&gt;Sui transactions are explicitly structured. You specify which objects you want to read, which you want to mutate, and which are pure inputs (like numbers or strings). The transaction declares its needs upfront, and the blockchain verifies that you have the right to access what you declared.&lt;/p&gt;

&lt;p&gt;A basic transaction might look like this in pseudocode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;Transaction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;object_id_1&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mutable&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;object_id_2&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;immutable&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="mi"&gt;0x123456&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;immutable&lt;/span&gt; &lt;span class="n"&gt;pure&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="nn"&gt;module&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;function&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;Before execution, Sui's transaction validation layer checks: does the sender own object_id_1? Is object_id_2 shared or owned by the sender? The pure value (a static address) requires no permission. Only if all checks pass does the transaction enter execution.&lt;/p&gt;

&lt;p&gt;This explicit input specification enables Sui to parallelize transaction execution aggressively. The network can run thousands of transactions in parallel if they touch disjoint sets of objects. Ethereum achieves sequentiality for safety; Sui achieves parallelism without sacrificing safety because ownership prevents conflicts by construction.&lt;/p&gt;

&lt;p&gt;Within a transaction, you pass objects to functions by reference or by value. Passing by value transfers ownership. Passing by immutable reference (&lt;code&gt;&amp;amp;T&lt;/code&gt;) allows reading but not mutation. Passing by mutable reference (&lt;code&gt;&amp;amp;mut T&lt;/code&gt;) allows both reading and mutation, and this is how you modify objects within a transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resource Safety and Scarcity
&lt;/h2&gt;

&lt;p&gt;Move's linear type system creates genuine scarcity on the blockchain. A resource cannot be duplicated, lost, or created out of thin air. Every token, every NFT, every key or certificate must flow through the system via explicit operations. This is enforced by the compiler, not by contract logic.&lt;/p&gt;

&lt;p&gt;Consider a token type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module example::token {
    use sui::object::{Self, UID};
    use sui::tx_context::TxContext;

    struct Token has key, store {
        id: UID,
        amount: u64,
    }

    public fun transfer(token: Token, to: address, ctx: &amp;amp;mut TxContext) {
        transfer::public_transfer(token, to);
    }

    public fun merge(t1: Token, t2: Token): Token {
        Token {
            id: t1.id,
            amount: t1.amount + t2.amount,
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, if you want to merge two tokens, you must write explicit logic to handle the merge. Once merged, the first token's ID is kept and the second is discarded. The compiler accepts this because we explicitly destroyed t2. There is no hidden path where t2 persists. There is no "ghost balance" that could linger unnoticed.&lt;/p&gt;

&lt;p&gt;If someone tries to call merge but then later use t2 again, the compiler rejects it with an error. The borrow checker catches this before the transaction is submitted. This guarantees at the language level that Token instances are accounted for.&lt;/p&gt;

&lt;p&gt;This contrasts sharply with Solidity, where token contracts use a mapping to track balances. If a contract has a bug in that mapping logic, tokens can be created or destroyed silently. The system has no intrinsic guarantee that total supply is preserved. With Move tokens, scarcity is a language property, not a contract invariant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smart Contract Patterns and Best Practices
&lt;/h2&gt;

&lt;p&gt;Building robust Sui contracts requires understanding how Move's features map to common patterns. The most important pattern is the separation between owned and shared state. Owned objects should be used for user-specific data: wallets, inventories, positions. Shared objects should be reserved for truly shared state: token reserves, global counters, orderbooks.&lt;/p&gt;

&lt;p&gt;A practical example is a simple escrow contract. In Ethereum, you might have a single contract that holds all escrowed assets in a mapping. In Sui, each escrow agreement is a separate object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module example::escrow {
    use sui::object::{Self, UID};
    use sui::tx_context::{Self, TxContext};
    use sui::transfer;

    struct EscrowAgreement&amp;lt;T: store&amp;gt; has key {
        id: UID,
        item: T,
        seller: address,
        buyer: address,
        price: u64,
    }

    public fun create_escrow&amp;lt;T: store&amp;gt;(
        item: T,
        seller: address,
        buyer: address,
        price: u64,
        ctx: &amp;amp;mut TxContext,
    ): EscrowAgreement&amp;lt;T&amp;gt; {
        EscrowAgreement {
            id: object::new(ctx),
            item,
            seller,
            buyer,
            price,
        }
    }

    public fun release&amp;lt;T: store&amp;gt;(
        escrow: EscrowAgreement&amp;lt;T&amp;gt;,
        _payment: Token,
        ctx: &amp;amp;mut TxContext,
    ): T {
        let EscrowAgreement { id, item, seller: _, buyer: _, price: _ } = escrow;
        object::delete(id);
        transfer::public_transfer(item, tx_context::sender(ctx));
        item
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, each escrow is a distinct object. The seller or a mediator can verify the agreement's terms before releasing it. Because the item is stored within the EscrowAgreement, it cannot be accessed or moved except through this contract's functions. The type system ensures that.&lt;/p&gt;

&lt;p&gt;The use of generics (like &lt;code&gt;&amp;lt;T: store&amp;gt;&lt;/code&gt;) is powerful in Sui. A single escrow contract can hold any type of asset, as long as that type has the store ability. This avoids the code duplication and bytecode bloat that comes from writing separate contracts for each asset type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing to Account-Based Models
&lt;/h2&gt;

&lt;p&gt;The conceptual gap between object-based and account-based systems manifests clearly in token handling. An Ethereum ERC-20 token is a contract that maintains a mapping from addresses to balances. Transferring requires calling the token contract, which updates the mapping. Parallel execution is difficult because every transfer touches shared state.&lt;/p&gt;

&lt;p&gt;A Sui token, by contrast, is a distributed collection of owned objects. Each Token object is owned by a specific address. Transferring a token means changing its owner field, which is an operation on that specific object. Thousands of different token transfers can happen in parallel if they involve different token objects.&lt;/p&gt;

&lt;p&gt;From a developer experience perspective, this means Sui contracts are smaller and more focused. You do not need to build elaborate permission systems because ownership is enforced at the protocol level. You do not need to worry about reentrancy because once you pass an object to another function, you cannot access it in your current scope. You do not need to track "total supply" in a central location because the language guarantees that resources are conserved.&lt;/p&gt;

&lt;p&gt;The learning curve is inverted compared to Ethereum. Beginners can write safer contracts faster because the language prevents whole categories of vulnerabilities. But developers must unlearn some habits: there is no centralized state to query, no permission modifiers to write, no balance checks that might be bypassed. Instead, developers work with object references and move values explicitly through call chains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Development Workflow
&lt;/h2&gt;

&lt;p&gt;Setting up a Sui development environment requires the Sui CLI and a local test validator. The typical workflow involves writing Move modules, testing them with Sui's built-in testing framework, and then deploying to testnet or mainnet.&lt;/p&gt;

&lt;p&gt;Testing in Sui is more explicit than in Ethereum. You construct test transactions, supply the objects they need, and verify the results. The Sui CLI provides a REPL where you can interact with live contracts on testnet, inspect object state, and simulate transactions before paying gas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#[test]
fun test_escrow_creation() {
    let mut scenario = test_scenario::begin(@0x1);
    let ctx = test_scenario::ctx(&amp;amp;mut scenario);

    let item = MyItem { id: object::new(ctx), value: 100 };
    let escrow = create_escrow(
        item,
        @0x1,
        @0x2,
        1000,
        ctx,
    );

    assert!(escrow.price == 1000, 0);
    test_scenario::end(scenario);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test_scenario module simulates a blockchain environment where you control who is performing transactions. You begin a scenario with a specific sender address, obtain the transaction context, and execute contract functions. After each step, you can inspect the resulting objects and verify invariants.&lt;/p&gt;

&lt;p&gt;Gas costs in Sui are predictable because the network computes them upfront based on transaction size and storage usage. Unlike Ethereum, where gas depends on the execution path taken, Sui charges based on the computational work declared in the transaction. This makes gas budgeting straightforward and eliminates the class of surprises where a transaction costs more than expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leveraging Move's Strengths in Production
&lt;/h2&gt;

&lt;p&gt;Production Sui contracts should follow patterns that leverage the language's safety guarantees. First, use owned objects for user-specific data. Do not create shared objects unless you need global coordination. Second, use generic types to write reusable contracts that work with multiple asset types. Third, use the module system to organize code logically; Move packages can contain multiple modules, and modules can depend on each other to form coherent abstractions.&lt;/p&gt;

&lt;p&gt;One pattern that simplifies contract design is the "Capability Object." A capability is a token (a struct with the key ability) that grants permission to perform an action. For example, an admin capability object proves that the holder is an administrator. Only the initial deployer receives this object, and they can transfer it to others or destroy it. This pattern moves authorization from function-level checks to object-level proofs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module example::admin {
    use sui::object::{Self, UID};
    use sui::tx_context::TxContext;

    struct AdminCap has key {
        id: UID,
    }

    public fun create_admin_cap(ctx: &amp;amp;mut TxContext): AdminCap {
        AdminCap {
            id: object::new(ctx),
        }
    }

    public fun verify_admin(_cap: &amp;amp;AdminCap) {
        // If we reach here, the caller passed a valid AdminCap
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Functions that require admin access take a reference to AdminCap as a parameter. Only someone who owns the AdminCap object can call these functions. The blockchain enforces this at the transaction validation layer. No runtime permission check is needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Security Implications of Structural Enforcement
&lt;/h2&gt;

&lt;p&gt;The shift from runtime permission checks to compile-time and protocol-level enforcement has profound security implications. Many of the vulnerabilities that plagued early smart contracts are impossible in Move because the language prevents them at compile time. You cannot accidentally reuse a value after passing it to another function. You cannot overflow an integer in safe mode. You cannot create or destroy resources outside of explicit operations.&lt;/p&gt;

&lt;p&gt;This does not mean Sui contracts are automatically secure. Logic errors are still possible. An escrow contract could have a bug in its release conditions. A token contract could mint more than intended if the minting logic is incorrect. But entire classes of vulnerabilities are eliminated by design. The developer can focus on business logic rather than fighting the language.&lt;/p&gt;

&lt;p&gt;Testing practices should reflect this. Because the type system prevents many bugs, tests can focus on correctness of business logic rather than defensive coding. You do not need exhaustive tests for integer overflow scenarios because Move prevents overflow at compile time. You can write clearer tests that specify what should happen in happy-path and edge cases, knowing the language is watching for memory safety and resource leaks.&lt;/p&gt;

&lt;p&gt;The object model also simplifies auditing. Because objects are self-contained and ownership is explicit, an auditor can trace value flows through the contract more directly. There is no hidden state scattered across multiple storage slots. No ambiguous permission logic buried in modifiers. Objects and functions declare their inputs and outputs explicitly, making the contract's surface area clear.&lt;/p&gt;

&lt;p&gt;Professional Web3 documentation and full-stack Next.js development work are available through my Fiverr profile at &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt;, should your project require technical writing or application development expertise.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>blockchain</category>
      <category>programming</category>
      <category>web3</category>
    </item>
    <item>
      <title>Wisp CMS and Next.js Blog Template Integration for Independent Developer Portfolios</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Mon, 20 Apr 2026 15:21:21 +0000</pubDate>
      <link>https://forem.com/mericcintosun/wisp-cms-and-nextjs-blog-template-integration-for-independent-developer-portfolios-e6m</link>
      <guid>https://forem.com/mericcintosun/wisp-cms-and-nextjs-blog-template-integration-for-independent-developer-portfolios-e6m</guid>
      <description>&lt;h2&gt;
  
  
  Building Portfolio Blogs with Wisp CMS and Next.js
&lt;/h2&gt;

&lt;p&gt;Independent developers need to demonstrate their technical knowledge and project experience. A well-designed blog serves as both a portfolio showcase and a platform for technical writing. Wisp CMS, paired with Next.js, provides a lightweight, performant approach to building content-driven developer blogs without the overhead of traditional headless CMS platforms.&lt;/p&gt;

&lt;p&gt;This article examines the Wisp CMS Next.js blog template, focusing on server-side optimizations, dark mode implementation, and RSS feed generation. These features solve specific problems that arise when independent developers need to maintain a production-grade blog while keeping complexity low and build times fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Wisp CMS Architecture
&lt;/h2&gt;

&lt;p&gt;Wisp CMS operates as a file-based content management system designed specifically for Next.js applications. Rather than storing content in a database, Wisp uses the filesystem as its primary content store. This approach eliminates the need for external services, reduces infrastructure costs, and makes content portable and version-controllable.&lt;/p&gt;

&lt;p&gt;The system reads Markdown and MDX files from a designated content directory, parses frontmatter metadata, and exposes this data through a JavaScript API that Next.js can consume during build time or request time. Content files live alongside application code in the repository, making the entire project self-contained.&lt;/p&gt;

&lt;p&gt;Wisp integrates directly with Next.js's file-based routing and build process. When a developer adds a new post to the content directory, Wisp automatically detects it and makes it available through API functions. This means there is no separate deployment process for content—pushing code to production automatically publishes new posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server Components and Performance Optimization
&lt;/h2&gt;

&lt;p&gt;Next.js server components represent a shift in how React applications render content. Server components execute on the server and send only the rendered HTML to the browser, rather than shipping JavaScript that runs client-side. This reduces the JavaScript payload sent to users and improves initial page load time.&lt;/p&gt;

&lt;p&gt;Wisp blog templates leverage server components by rendering blog list pages and individual post pages entirely on the server. When a user requests the blog homepage, the server fetches all posts from the filesystem, renders the HTML, and sends the complete page to the browser. The browser receives no React component tree to hydrate, no state management code, and no unnecessary JavaScript.&lt;/p&gt;

&lt;p&gt;Consider the practical difference. A traditional client-side rendered blog loads a JavaScript bundle that contains the entire React framework, routing logic, state management, and component code. The browser must parse and execute this bundle before rendering anything. A server component-based blog sends only the HTML the user needs to see. The bundle size difference is substantial: server-rendered blog pages ship 40-60% less JavaScript than their client-rendered equivalents, measured using standard bundling tools.&lt;/p&gt;

&lt;p&gt;The performance gain extends beyond initial load. Server components avoid the "JavaScript execution cost" that impacts slower devices. On a mid-range Android phone with limited CPU power, executing 200KB of JavaScript takes measurable time. The same content delivered as pre-rendered HTML displays instantly.&lt;/p&gt;

&lt;p&gt;Implementing this in a Wisp Next.js blog requires marking post list and detail pages as server components. This is the default behavior in the Next.js app directory—components are server components unless explicitly marked as client components with the &lt;code&gt;"use client"&lt;/code&gt; directive.&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;// app/blog/page.js - rendered on server&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getPosts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@wisp-cms/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;PostCard&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/components/PostCard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BlogPage&lt;/span&gt;&lt;span class="p"&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;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPosts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blog-list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PostCard&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;))}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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;This page executes entirely on the server. The &lt;code&gt;getPosts()&lt;/code&gt; function reads from the filesystem, and the component tree renders to HTML without any client-side JavaScript. The returned HTML includes the complete blog list structure, ready for the browser to display.&lt;/p&gt;

&lt;p&gt;Individual post pages follow the same pattern. The server fetches post metadata and content, renders the Markdown or MDX to HTML, and sends the final document to the browser.&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;// app/blog/[slug]/page.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getPostBySlug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getPosts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@wisp-cms/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;MDXContent&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/components/MDXContent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateStaticParams&lt;/span&gt;&lt;span class="p"&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;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPosts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PostPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&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;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPostBySlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MDXContent&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/article&lt;/span&gt;&lt;span class="err"&gt;&amp;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;The &lt;code&gt;generateStaticParams()&lt;/code&gt; function tells Next.js to pre-render every post at build time. When the build process runs, it fetches all posts, renders each one as static HTML, and outputs files that serve instantly when users request them. This is static site generation—every post becomes a pre-built HTML file.&lt;/p&gt;

&lt;p&gt;Static generation provides the best performance for blogs. The server does no work at request time. Users receive cached HTML files served by a CDN. Load times drop to single-digit milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdown and MDX Processing
&lt;/h2&gt;

&lt;p&gt;Wisp supports both Markdown and MDX—a format that combines Markdown syntax with embedded React components. This distinction matters for blog content.&lt;/p&gt;

&lt;p&gt;Markdown files contain only text, headings, code blocks, and standard formatting. A Markdown processor converts this to HTML. This approach works well for most blog posts: the content is safe, fast to parse, and produces semantically clean HTML.&lt;/p&gt;

&lt;p&gt;MDX extends Markdown by allowing developers to embed React components directly in content files. This enables dynamic interactivity within posts: interactive code editors, data visualizations, embedded forms, and custom components that respond to user input.&lt;/p&gt;

&lt;p&gt;The trade-off exists in build complexity and client-side JavaScript. A post written in Markdown renders completely on the server—no client-side code is needed. A post written in MDX must include the React component definitions and any client state management those components require. This increases the JavaScript bundle size.&lt;/p&gt;

&lt;p&gt;For developer portfolios, the choice depends on the content style. Educational posts with code examples work fine in Markdown. Interactive posts that showcase specific functionality benefit from MDX components.&lt;/p&gt;

&lt;p&gt;Wisp handles both formats transparently. The blog template can include posts in either format in the same blog directory. The system detects the file type and processes it accordingly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Building&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Smart&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Contract&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Auditor"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Implementing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;automated&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;security&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;checks"&lt;/span&gt;
&lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Building a Smart Contract Auditor&lt;/span&gt;

When building production smart contracts, security analysis must be systematic. This post walks through the architecture of an automated auditor.

&lt;span class="gu"&gt;## The Audit Pipeline&lt;/span&gt;

The auditor processes contracts in stages:
&lt;span class="p"&gt;
1.&lt;/span&gt; Lexical analysis tokenizes the Solidity code
&lt;span class="p"&gt;2.&lt;/span&gt; Syntax analysis builds an abstract syntax tree
&lt;span class="p"&gt;3.&lt;/span&gt; Semantic analysis performs type checking and flow analysis
&lt;span class="p"&gt;4.&lt;/span&gt; Pattern matching identifies known vulnerabilities

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Markdown file contains standard prose and code blocks. When Wisp processes it, the frontmatter metadata (title, description, published status) becomes accessible through the API. The Markdown content converts to HTML.&lt;/p&gt;

&lt;p&gt;An MDX equivalent could include interactive components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Building a Smart Contract Auditor&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Implementing automated security checks&lt;/span&gt;
&lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

import { CodeDifference } from '@/components/CodeDifference';

&lt;span class="gh"&gt;# Building a Smart Contract Auditor&lt;/span&gt;

&amp;lt;CodeDifference 
  before={vulnerableCode} 
  after={fixedCode}
/&amp;gt;

When building production smart contracts...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the &lt;code&gt;CodeDifference&lt;/code&gt; component renders interactively, allowing users to toggle between vulnerable and fixed versions of code. This requires React on the client to function, increasing the JavaScript bundle size.&lt;/p&gt;

&lt;p&gt;For independent developer portfolios, Markdown alone provides sufficient functionality in most cases. The focus should remain on clear technical writing rather than interactive components. Interactive elements distract from the content and increase maintenance burden—components must remain compatible with the blog template across framework versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dark Mode Implementation
&lt;/h2&gt;

&lt;p&gt;Dark mode has become standard for developer-focused applications. Implementing it properly requires handling both server-side and client-side rendering, persisting user preference, and ensuring all text and components maintain sufficient contrast in both themes.&lt;/p&gt;

&lt;p&gt;The Wisp Next.js template implements dark mode using CSS custom properties (variables) that change based on a theme class on the document root element. This approach separates theme logic from component styling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* styles/theme.css */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e5e7eb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-code-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f3f4f6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"dark"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0a0a0a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#2d2d2d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-code-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1a1a1a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-background&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;code&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-code-bg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-border&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;Components reference these variables through standard CSS. This means a single CSS file controls theme colors across the entire application.&lt;/p&gt;

&lt;p&gt;The critical challenge with dark mode in Next.js is preventing flash of unstyled content (FOUC) during page load. When a user visits the site with dark mode enabled, the initial HTML sent by the server renders in light mode (the default). The JavaScript then loads, detects the user's preference, and switches to dark mode. The user sees a flash of light mode before dark mode appears.&lt;/p&gt;

&lt;p&gt;Solving this requires detecting the theme preference before rendering any content. Wisp's Next.js template uses a small script injected into the document head, before any styling or component rendering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&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;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(prefers-color-scheme: dark)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&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="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})();&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script runs synchronously before the page renders. It checks localStorage for a stored theme preference. If no preference exists, it checks the system's color scheme preference using the media query API. Then it sets the theme attribute on the document root immediately.&lt;/p&gt;

&lt;p&gt;Because this script runs before any visual content renders, the correct theme applies from the start. No flash occurs.&lt;/p&gt;

&lt;p&gt;A client-side component handles theme switching when the user clicks a toggle button:&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;// components/ThemeToggle.js&lt;/span&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="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;ThemeToggle&lt;/span&gt;&lt;span class="p"&gt;()&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;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTheme&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMounted&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="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;currentTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentTheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setMounted&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="p"&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;toggleTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;newTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&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="s1"&gt;dark&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="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newTheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newTheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newTheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;toggleTheme&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Toggle theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&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="s1"&gt;🌙&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="s1"&gt;☀️&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;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;The component must only render after mounting (the &lt;code&gt;if (!mounted) return null&lt;/code&gt; check). This prevents server-side rendering the theme toggle in one state while the client expects another. The toggle updates both the DOM attribute and localStorage, so the preference persists across sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  RSS Feed Generation
&lt;/h2&gt;

&lt;p&gt;RSS feeds provide a subscription mechanism for blog readers. Developers often subscribe to relevant blogs using RSS readers, making RSS support valuable for reaching technical audiences.&lt;/p&gt;

&lt;p&gt;Wisp generates RSS feeds by collecting all published posts, formatting them according to RSS specification, and serving them from a well-known URL (typically &lt;code&gt;/feed.xml&lt;/code&gt; or &lt;code&gt;/rss.xml&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;RSS feed generation happens at build time using a simple Node.js script. The script fetches all posts from Wisp, iterates through them, and generates valid RSS XML:&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;// scripts/generate-rss.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getPosts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@wisp-cms/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&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;SITE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://yourdomain.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateRSS&lt;/span&gt;&lt;span class="p"&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;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPosts&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;published&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;published&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&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;rssItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;published&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;item&amp;gt;
      &amp;lt;title&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;escapeXml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/title&amp;gt;
      &amp;lt;description&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;escapeXml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/description&amp;gt;
      &amp;lt;link&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/link&amp;gt;
      &amp;lt;guid&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/guid&amp;gt;
      &amp;lt;pubDate&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUTCString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/pubDate&amp;gt;
    &amp;lt;/item&amp;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;join&lt;/span&gt;&lt;span class="p"&gt;(&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;rss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;
&amp;lt;rss version="2.0"&amp;gt;
  &amp;lt;channel&amp;gt;
    &amp;lt;title&amp;gt;Your Blog Title&amp;lt;/title&amp;gt;
    &amp;lt;link&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/link&amp;gt;
    &amp;lt;description&amp;gt;Your blog description&amp;lt;/description&amp;gt;
    &amp;lt;language&amp;gt;en-us&amp;lt;/language&amp;gt;
    &amp;lt;lastBuildDate&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toUTCString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/lastBuildDate&amp;gt;
    &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rssItems&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  &amp;lt;/channel&amp;gt;
&amp;lt;/rss&amp;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;publicDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&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="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;feed.xml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RSS feed generated at /feed.xml&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;escapeXml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;amp;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;amp;&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;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;lt;&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;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/"/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;quot;&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;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/'/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;apos;&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;generateRSS&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script integrates into the Next.js build process by adding it to the &lt;code&gt;package.json&lt;/code&gt; build script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node scripts/generate-rss.js &amp;amp;&amp;amp; next build"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time the application builds, the RSS feed regenerates with the latest posts. The generated XML file sits in the public directory, where Next.js serves it as a static asset.&lt;/p&gt;

&lt;p&gt;RSS feeds require proper XML formatting and date handling. The script escapes special XML characters in post titles and descriptions to prevent invalid XML. It formats publication dates in RFC 2822 format (the RSS standard), using &lt;code&gt;toUTCString()&lt;/code&gt; to ensure consistency regardless of the server's timezone.&lt;/p&gt;

&lt;p&gt;For maximum discoverability, the blog layout template should include a link tag in the document head:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/rss+xml"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/feed.xml"&lt;/span&gt; &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"Your Blog RSS Feed"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells feed readers where to find the RSS feed automatically. When readers visit the site, their reader application detects the feed link and offers to subscribe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advantages Over MDX-Based Systems
&lt;/h2&gt;

&lt;p&gt;Several blog systems use MDX as their primary content format, combining Markdown writing with embedded React components. While flexible, this approach introduces complexity that doesn't serve independent developer portfolios well.&lt;/p&gt;

&lt;p&gt;MDX-based systems require the entire React framework to ship to the browser for each post. Interactive components need client-side JavaScript to function, increasing bundle size. A post with a single interactive code block still requires loading the full React runtime and all component dependencies. This overhead accumulates across pages.&lt;/p&gt;

&lt;p&gt;The Wisp template keeps client-side JavaScript to a minimum. Most pages render entirely on the server. Only interactive elements like the theme toggle require client-side code. A simple blog post is pure HTML—no JavaScript executes on the user's browser.&lt;/p&gt;

&lt;p&gt;Build time differs significantly. MDX compilation adds processing overhead during the build. Every MDX file must be parsed, AST-transformed, and compiled. For a blog with 50-100 posts, this can extend build times to several seconds or more. Wisp's filesystem approach with standard Markdown processes faster, typically completing blog builds in under one second.&lt;/p&gt;

&lt;p&gt;Content migration becomes simpler with Markdown. If an independent developer needs to switch blog platforms in the future, Markdown files remain compatible across systems. MDX files tie content to the React ecosystem, making portability harder.&lt;/p&gt;

&lt;p&gt;For most independent developer blogs, the advantages of simplicity and performance outweigh the flexibility of embedded interactive components. Technical writing benefits from focused, readable prose and clear code examples rather than interactive distractions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up a Blog with the Wisp Template
&lt;/h2&gt;

&lt;p&gt;Creating a new blog with Wisp and Next.js starts with scaffolding a project from the official template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-next-app@latest my-blog &lt;span class="nt"&gt;--template&lt;/span&gt; https://github.com/wisp-cms/nextjs-blog-template
&lt;span class="nb"&gt;cd &lt;/span&gt;my-blog
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command clones the template repository, installs dependencies, and starts the development server. The template includes a &lt;code&gt;content/blog&lt;/code&gt; directory with example posts in Markdown format.&lt;/p&gt;

&lt;p&gt;Adding a new post requires creating a Markdown file in the &lt;code&gt;content/blog&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Understanding Async/Await in Solidity&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A guide to asynchronous patterns in smart contracts&lt;/span&gt;
&lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2024-01-15&lt;/span&gt;
&lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Understanding Async/Await in Solidity&lt;/span&gt;

Solidity smart contracts operate in a synchronous execution model...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wisp detects the new file immediately in development mode. Refreshing the browser shows the new post in the blog list. The post is accessible at &lt;code&gt;/blog/understanding-async-await-in-solidity&lt;/code&gt; (the slug is derived from the filename).&lt;/p&gt;

&lt;p&gt;For deployment, the typical approach is pushing the repository to a hosting service like Vercel, GitHub Pages, or a self-hosted server. The build process generates static HTML files for all posts and deploys them alongside the Next.js application code.&lt;/p&gt;

&lt;p&gt;Vercel provides the smoothest deployment experience for Next.js applications. Connecting a GitHub repository to Vercel enables automatic deployments: every push to the main branch triggers a build, runs the RSS generation script, and deploys the updated site. New blog posts published via a repository push go live within seconds.&lt;/p&gt;

&lt;p&gt;The combination of server components, static generation, and filesystem-based content storage makes Wisp blogs ideal for independent developers. The setup requires minimal configuration, scales to thousands of posts without performance degradation, and keeps deployment simple.&lt;/p&gt;




&lt;p&gt;For Web3 documentation needs or full-stack Next.js development work, I'm available for consulting. Visit my Fiverr profile at &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt; to discuss your project.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>nextjs</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>AI-Assisted Debugging in Next.js 16 with Experimental Agent DevTools</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Sat, 18 Apr 2026 15:21:20 +0000</pubDate>
      <link>https://forem.com/mericcintosun/ai-assisted-debugging-in-nextjs-16-with-experimental-agent-devtools-1obg</link>
      <guid>https://forem.com/mericcintosun/ai-assisted-debugging-in-nextjs-16-with-experimental-agent-devtools-1obg</guid>
      <description>&lt;p&gt;Next.js 16 introduces a foundational shift in how developers and AI agents interact with application state and debugging information. The &lt;strong&gt;Experimental Agent DevTools&lt;/strong&gt; framework enables AI assistants to access React DevTools protocols and browser logs through a terminal-based interface, transforming the debugging workflow from a human-centric process into a collaborative one where AI agents can autonomously identify issues, suggest fixes, and accelerate development cycles.&lt;/p&gt;

&lt;p&gt;This capability addresses a critical bottleneck in modern development. When a browser error occurs, developers historically had to manually describe the problem to an AI assistant, who would then generate code changes based on incomplete information. With Agent DevTools, the AI agent receives structured, real-time access to component state, props, and console output, eliminating the translation step and reducing debugging time from hours to minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Browser Log Forwarding Works
&lt;/h2&gt;

&lt;p&gt;Browser Log Forwarding is the foundational mechanism that pipes console output, error messages, and runtime warnings directly from the browser to the terminal where the development server runs. Rather than developers opening DevTools and copying error messages, the system captures all browser-side events and streams them through a dedicated channel.&lt;/p&gt;

&lt;p&gt;The forwarding system works through Next.js's internal development server infrastructure. When a page loads in the browser, a client-side listener establishes a WebSocket connection back to the dev server. This connection is not the same as the hot module replacement (HMR) channel; it is a separate stream dedicated to diagnostic information. Every console.log(), console.error(), and uncaught exception that occurs in the browser gets serialized and transmitted to the server process.&lt;/p&gt;

&lt;p&gt;The dev server writes these logs to stderr, prefixed with metadata indicating the source. A developer running Next.js 16 in development mode will see browser output interleaved with server logs, creating a unified view of application behavior. This is particularly valuable in Next.js applications that use both client components and server components, because it eliminates the need to switch between the terminal and browser DevTools to understand what is happening on each side.&lt;/p&gt;

&lt;p&gt;The forwarding respects log levels. Console warnings appear as warnings in the terminal, errors appear as errors, and info messages maintain their level. The system also captures stack traces from thrown exceptions, which helps developers pinpoint the exact line of code that caused a failure without needing to inspect the browser's call stack directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling Agent DevTools in Your Development Environment
&lt;/h2&gt;

&lt;p&gt;Experimental Agent DevTools is not enabled by default; it requires explicit configuration in your Next.js project. The setup process involves modifying your next.config.js file to enable the experimental flag and, optionally, configuring how the agent communicates with your application.&lt;/p&gt;

&lt;p&gt;Add the following to your next.config.js:&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="cm"&gt;/** @type {import('next').NextConfig} */&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;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;agentDevTools&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="p"&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;nextConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once this flag is set, the development server automatically exposes an endpoint that AI agents can call to retrieve debugging information. The agent can query the current state of React components, access the component tree, and read the browser logs that have been forwarded to the server.&lt;/p&gt;

&lt;p&gt;When you restart your dev server with this configuration, you will notice a new line in the output indicating that Agent DevTools is active. The server listens on a local endpoint (typically &lt;a href="http://localhost:3000/__agent-devtools" rel="noopener noreferrer"&gt;http://localhost:3000/__agent-devtools&lt;/a&gt;) that accepts structured queries about application state. This endpoint is disabled in production builds and only active during development.&lt;/p&gt;

&lt;p&gt;If you are using a third-party AI assistant or building your own integration, you will need the endpoint URL and any authentication details. For local development with Vercel's built-in assistants, authentication happens automatically through the dev server's process model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting AI Agents to Your Application State
&lt;/h2&gt;

&lt;p&gt;An AI agent that is integrated with Agent DevTools can make API requests to inspect the real-time state of your Next.js application without human intervention. Rather than working with outdated code snippets or a developer's description of a bug, the agent queries the actual component state and console logs from the last few seconds.&lt;/p&gt;

&lt;p&gt;The agent can request a full component tree snapshot, which includes the hierarchy of all mounted components, their current props, and their internal state. For a complex application with nested providers, conditional rendering, and state management libraries like Redux or Zustand, this snapshot provides clarity about what the application actually looks like at the moment the bug occurred.&lt;/p&gt;

&lt;p&gt;When a browser error occurs, the agent can immediately fetch the forwarded logs and examine the full error message along with surrounding console output. If the error is a React-specific issue like a missing dependency in a useEffect hook or a stale closure in a callback, the agent can read the warning directly from the log stream and correlate it with the component tree to understand which component is problematic.&lt;/p&gt;

&lt;p&gt;The agent can also trigger actions. For instance, if the agent suspects that the bug only manifests under a specific user interaction, it can request that the dev server simulate a user action or change a query parameter, then observe how the application state changes. This capability turns debugging into an interactive process rather than a single-shot question-and-answer session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structure of Agent DevTools API Responses
&lt;/h2&gt;

&lt;p&gt;The Agent DevTools endpoint returns structured JSON responses that follow a consistent schema. Understanding this schema is essential for building reliable integrations or debugging why an agent is not receiving the information it needs.&lt;/p&gt;

&lt;p&gt;A component tree query returns an object containing an array of component nodes. Each node has a displayName (the component's name), a unique ID, a list of child node IDs, the current props object, and internal state if applicable. For functional components, state is represented as an array of hooks with their current values. For class components, state is a flat object. This structure allows an agent to traverse the tree programmatically and locate a specific component even in deeply nested applications.&lt;/p&gt;

&lt;p&gt;Forwarded logs are returned as an array of log entries. Each entry contains a timestamp, the log level (log, warn, error), the message content, and the stack trace if the entry is an exception. Some entries also include serialized objects or arrays that were logged, allowing the agent to inspect complex data structures that were printed to the console.&lt;/p&gt;

&lt;p&gt;Error information is particularly detailed. When the agent queries for errors, it receives the full exception object including the error message, the error type (ReferenceError, TypeError, etc.), and the complete call stack with file names and line numbers. For React-specific errors like "Cannot read properties of undefined", the agent can match the error against the component tree to identify which component triggered the error.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AI Agents Accelerate PR Review and Debugging Workflows
&lt;/h2&gt;

&lt;p&gt;The practical value of Agent DevTools emerges in how it changes the pace of collaborative debugging between developers and AI assistants. Traditionally, when a developer files a bug report in a pull request, describing a flaky test or a race condition, an AI assistant reading the code can only speculate about the root cause. With Agent DevTools, the AI agent can run the test locally, observe the exact sequence of state changes, and pinpoint where the failure occurs.&lt;/p&gt;

&lt;p&gt;Consider a scenario where a Next.js application has a form with asynchronous validation. A developer submits a PR claiming the form sometimes loses focus when validation completes. Without Agent DevTools, the AI assistant can read the code and identify potential issues like race conditions in cleanup functions, but cannot verify if that is the actual problem. With Agent DevTools active, the agent can programmatically interact with the form, trigger validation, observe the component state changes in real time, and see whether the focus event is indeed lost and at what exact point in the lifecycle it disappears.&lt;/p&gt;

&lt;p&gt;In documentation review cycles, Agent DevTools enables agents to validate that code examples actually work. An agent can take a code example from a documentation file, execute it in an isolated test environment, and verify that the output matches what the documentation claims. If an API example is outdated or incorrect, the agent detects the discrepancy immediately rather than waiting for a user to report the issue weeks later.&lt;/p&gt;

&lt;p&gt;For pull requests that add new features, an AI agent can review not just the code syntax but the actual runtime behavior. The agent runs the feature, observes the component state, checks for console warnings or errors, and provides actionable feedback on performance bottlenecks or edge cases that code review alone would miss. This shifts PR review from a textual, static process to a dynamic one that catches behavioral issues before they reach staging.&lt;/p&gt;

&lt;p&gt;The speed improvement is tangible. A debugging session that might take a developer two hours of manual testing and back-and-forth with an AI assistant can often be resolved in ten to fifteen minutes when the agent has direct access to the application state. The agent's ability to check the actual behavior against the expected behavior, rather than working from descriptions and speculation, eliminates the feedback loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Integration Patterns
&lt;/h2&gt;

&lt;p&gt;Developers integrating Agent DevTools into their workflow have several options depending on their use case. The simplest approach is to enable the feature in development.js and rely on any IDE or coding assistant that Vercel or third parties provide out of the box. These assistants connect automatically and require no additional configuration.&lt;/p&gt;

&lt;p&gt;For teams building custom AI integrations, the pattern involves exposing the Agent DevTools endpoint through your development server, then building a client library that an external AI service can call. The external service makes HTTP requests to fetch component state or logs, processes the responses, and generates or refines code based on what it learns.&lt;/p&gt;

&lt;p&gt;A more advanced pattern is to create an AI-driven test runner. This runner spawns a dev server with Agent DevTools enabled, then uses the agent to systematically interact with different parts of the application, recording state changes and generating test cases based on observed behavior. The agent can identify edge cases that traditional test writing might miss because it sees the actual state mutations rather than working from human-written scenarios.&lt;/p&gt;

&lt;p&gt;Debugging a TypeScript-based Next.js application with Agent DevTools yields particular value. The agent can see that a prop passed to a component has a type that does not match the component's PropTypes or TypeScript definition, even if TypeScript compilation succeeded. This catches bugs at runtime that static analysis alone might miss, especially in cases where type assertions (as) have been used to silence TypeScript warnings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and When Agent DevTools Falls Short
&lt;/h2&gt;

&lt;p&gt;Agent DevTools significantly improves debugging velocity, but it does not eliminate the need for humans in the loop. The system works best for behavioral bugs and state management issues. It is less useful for performance problems that require profiling data, network request analysis, or database query optimization.&lt;/p&gt;

&lt;p&gt;Browser Log Forwarding captures application-level logs, not network traffic. If an AI agent needs to debug why an API request is failing, it can see the JavaScript error that resulted, but not the HTTP response status or headers. For that information, a developer still needs to open the browser's Network tab or configure request logging in the application itself.&lt;/p&gt;

&lt;p&gt;Agent DevTools operates on the current state. If a bug manifests only under specific timing conditions (a race condition that occurs once every thousand interactions), the agent cannot easily reproduce it unless the conditions are explicitly parameterized. The agent is not a substitute for tools like Jest or Playwright for deterministic, reproducible testing.&lt;/p&gt;

&lt;p&gt;The forwarded log stream is also bounded by memory. Very chatty applications that log thousands of messages per second may lose older log entries as newer ones arrive. For debugging long-running applications or production-like workloads, developers should still configure persistent logging at the application level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating Agent DevTools with Type Checking and Linting
&lt;/h2&gt;

&lt;p&gt;An AI agent with access to Agent DevTools should not be its only tool. The most effective debugging workflows combine agent-driven runtime introspection with static analysis from TypeScript and ESLint. When an agent detects a runtime error, it can correlate that error with the static types defined in your codebase to understand whether the issue is a type mismatch or a logic error.&lt;/p&gt;

&lt;p&gt;If a component receives a prop that is typed as &lt;code&gt;string | null&lt;/code&gt;, and the agent observes that the prop value is actually &lt;code&gt;undefined&lt;/code&gt;, the agent can immediately recognize that the incoming code violates the type contract. This is valuable for catching bugs where TypeScript compilation succeeded but the runtime type is actually different.&lt;/p&gt;

&lt;p&gt;ESLint rules for React, such as eslint-plugin-react's rules for hooks dependencies, can be automated and triggered by the agent. When the agent observes a stale closure or a missing dependency, it can run the linter and show the developer exactly which rule was violated and what the fix should be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Reproducible Test Cases from Agent Observations
&lt;/h2&gt;

&lt;p&gt;One of the highest-value applications of Agent DevTools is using the agent's observations to generate automated test cases. When an agent detects a bug, it has recorded the exact sequence of state changes and user interactions that triggered it. That sequence can be translated into a Playwright or Cypress test that reproduces the issue deterministically.&lt;/p&gt;

&lt;p&gt;For example, if the agent observes that a form validation fails unexpectedly, it can generate a test that sets the form field to the same value, triggers the validation, and asserts that the error message appears. The test is based on actual observed behavior, not a hypothesis about what might go wrong.&lt;/p&gt;

&lt;p&gt;This capability is particularly valuable for regression testing. Before submitting a fix to a bug, a developer can have the agent generate a test case that verifies the fix. That test becomes part of the suite and prevents the bug from recurring in future changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging Server Components and Server Actions
&lt;/h2&gt;

&lt;p&gt;Next.js 16's support for Server Components and Server Actions complicates debugging because errors can occur on either the server or the client, or in the serialization boundary between them. Agent DevTools operates primarily on the client side, but it can still provide value for diagnosing server-related issues.&lt;/p&gt;

&lt;p&gt;When a Server Action fails, the error message and stack trace are forwarded to the client and logged there. The agent can read that forwarded error and help identify which Server Action failed and why. For serialization errors (e.g., trying to send a function or non-serializable object from server to client), the agent can see the error message and guide the developer toward the correct fix.&lt;/p&gt;

&lt;p&gt;Debugging Server Component rendering failures is harder because those errors occur on the server before the component ever reaches the client. Here, traditional server-side logging is still necessary. However, Agent DevTools can help with the client-side consequences of a server failure. If a Server Component fails to render and sends an error boundary fallback to the client, the agent can observe that fallback and help diagnose what went wrong on the server based on the error details that were included in the serialized response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;p&gt;Enabling Agent DevTools adds a small overhead to development. The WebSocket connection for log forwarding is persistent but lightweight. The JSON serialization of component state adds a few milliseconds when the agent queries the state snapshot, which is imperceptible in most cases.&lt;/p&gt;

&lt;p&gt;The forwarded logs are held in memory on the dev server. For typical development workloads with hundreds of logs per minute, this adds a few megabytes of memory usage. Applications that log aggressively should be mindful that very old logs will be discarded to keep memory usage bounded.&lt;/p&gt;

&lt;p&gt;In production builds, Agent DevTools is completely absent. The experimental flag is a compile-time configuration; it does not add code to the production bundle. Developers can leave the flag enabled in their next.config.js without any performance penalty in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Directions and Emerging Patterns
&lt;/h2&gt;

&lt;p&gt;The Experimental Agent DevTools framework is designed to evolve. Future versions may extend the capabilities to include time-travel debugging (rewinding state to a previous moment), collaborative debugging sessions where multiple agents or developers work on the same problem simultaneously, and deeper integration with CI/CD pipelines to run agent-assisted debugging automatically on failing tests.&lt;/p&gt;

&lt;p&gt;Teams that adopt Agent DevTools early should expect the API to change slightly as it moves from experimental to stable status. Breaking changes are possible, though Vercel typically maintains backward compatibility for widely-used features. Keeping a clear abstraction layer between your agent integration and the underlying Agent DevTools API makes it easier to adapt when updates arrive.&lt;/p&gt;

&lt;p&gt;The most successful teams are those that treat Agent DevTools as one component of a broader debugging toolkit, not a replacement for logging, monitoring, and traditional testing. The agent excels at interactive, real-time investigation of live bugs. Structured logging and monitoring excel at understanding systemic issues and performance patterns. Together, they cover the full spectrum of debugging needs.&lt;/p&gt;

&lt;p&gt;For professional Web3 documentation or full-stack Next.js development support, my Fiverr profile at &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt; is available for consultation.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>react</category>
    </item>
    <item>
      <title>Supply Chain Security in Next.js JavaScript Files with Subresource Integrity</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Thu, 16 Apr 2026 15:21:20 +0000</pubDate>
      <link>https://forem.com/mericcintosun/supply-chain-security-in-nextjs-javascript-files-with-subresource-integrity-4ifk</link>
      <guid>https://forem.com/mericcintosun/supply-chain-security-in-nextjs-javascript-files-with-subresource-integrity-4ifk</guid>
      <description>&lt;h2&gt;
  
  
  Understanding Subresource Integrity and Its Security Role
&lt;/h2&gt;

&lt;p&gt;Subresource Integrity (SRI) is a mechanism that allows browsers to verify that fetched resources have not been altered in transit or on a compromised server. The browser maintains a cryptographic hash of the expected resource and compares it to the resource it receives before executing any code. If the hashes do not match, the browser refuses to load the resource entirely. This prevents a critical attack vector in modern web applications: compromise of third-party CDNs, package registries, or even your own origin server.&lt;/p&gt;

&lt;p&gt;The threat model here is precise. An attacker who gains control over the CDN serving your JavaScript files can inject arbitrary code into those files. Without SRI, users' browsers would execute this malicious code with full application privileges. SRI breaks this chain by making it cryptographically impossible for an attacker to modify the resource without the publisher knowing about it and issuing a new signed hash.&lt;/p&gt;

&lt;p&gt;Next.js applications inherently rely on serving JavaScript bundles from a CDN or origin server. Every page load, every client-side navigation, and every dynamic import pulls JavaScript from the network. The larger and more distributed your application, the more servers and intermediaries touch your code before it reaches a user's browser. SRI gives you a mechanism to guarantee end-to-end integrity from source to execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mechanics of SRI: Hash Algorithms and Implementation
&lt;/h2&gt;

&lt;p&gt;SRI relies on cryptographic hashing to work. The most commonly supported algorithms are SHA-256, SHA-384, and SHA-512. When you generate an SRI hash, you compute the hash of the file content, encode it in base64, and prefix it with the algorithm identifier. A complete SRI hash looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sha384-abcdefg1234567890=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The format is &lt;code&gt;algorithm-base64encodedHash&lt;/code&gt;. The browser extracts the algorithm from this string and computes the same hash over the downloaded resource. If the computed value matches the provided value, the resource loads. If it does not match, the resource is rejected and a security error is raised in the console.&lt;/p&gt;

&lt;p&gt;Multiple hash algorithms can be specified in a single integrity attribute as a space-separated list. The browser uses the first algorithm it recognizes, so you might write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script
  &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.example.com/app-bundle.js"&lt;/span&gt;
  &lt;span class="na"&gt;integrity=&lt;/span&gt;&lt;span class="s"&gt;"sha384-ABC... sha256-DEF..."&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides compatibility with older browsers that might not support SHA-384 while still offering stronger hashing on modern browsers. The browser will use SHA-384 if available; otherwise it falls back to SHA-256.&lt;/p&gt;

&lt;p&gt;Computing these hashes is not a manual process. Build tools and package managers automate this. When Next.js builds your application with SRI support enabled, it computes the hash of each output bundle and embeds the hash into the HTML that references it. The development workflow becomes seamless: build, hash, embed, deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js 16.2 and Turbopack Integration
&lt;/h2&gt;

&lt;p&gt;Next.js 16.2 introduced native SRI support through improvements to its build system. The Turbopack bundler, which powers Next.js builds in recent versions, gained the ability to compute SRI hashes for all output bundles and automatically insert the integrity attributes into the HTML served to clients.&lt;/p&gt;

&lt;p&gt;This integration works by default when you build a Next.js application for production. The build process analyzes each JavaScript bundle, computes its SHA-384 hash (or SHA-256 depending on configuration), and writes the integrity attribute directly into the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags in your HTML output. No manual configuration is required for basic functionality, though fine-grained control is available for advanced use cases.&lt;/p&gt;

&lt;p&gt;The Turbopack bundler is significantly faster than Webpack, which is the historical bundler in Next.js applications. Faster builds mean you can iterate on security configurations more easily and regenerate SRI hashes as part of your normal deployment pipeline. The performance improvement is not merely a convenience; it enables more aggressive security practices. If your build takes two hours, you are unlikely to regenerate SRI hashes on every change. If your build takes two minutes, regenerating hashes becomes routine.&lt;/p&gt;

&lt;p&gt;To verify that SRI is being applied to your bundles, inspect the HTML output after building. The &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags should contain &lt;code&gt;integrity="sha384-..."&lt;/code&gt; attributes. You can check this with a simple curl or by opening your deployed page and viewing the page source in your browser's developer tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threat Model: Third-Party CDN Compromise
&lt;/h2&gt;

&lt;p&gt;The concrete threat that SRI defends against is compromise of a third-party content delivery network. Many applications serve JavaScript from CDNs operated by companies other than themselves: Cloudflare, AWS CloudFront, Azure CDN, or others. These CDNs are high-value targets because they serve content for millions of websites. A breach that goes undetected for even a few hours can affect millions of users across thousands of applications.&lt;/p&gt;

&lt;p&gt;In October 2023, the cdn.jsdelivr.net service experienced a partial outage, but more importantly it demonstrated the scale of exposure: a single CDN serves JavaScript to a significant portion of the web. If an attacker gained write access to this CDN, they could modify any file served through it. Users would download and execute the modified code without any warning or verification.&lt;/p&gt;

&lt;p&gt;SRI does not prevent the compromise of the CDN itself. Attackers can still break into servers and modify files. What SRI does is make the compromise detectable and unusable from the attacker's perspective. If the attacker modifies a file that has an SRI hash in the HTML, users' browsers will compute a different hash, the verification will fail, and the browser will refuse to load the resource. The application breaks, which triggers alerts and investigation. The attacker gains nothing because the malicious code never executes.&lt;/p&gt;

&lt;p&gt;This asymmetry is the core value proposition of SRI. The attacker must either avoid triggering the integrity failure (which means they cannot actually modify the code), or they trigger it immediately and compromise is detected in real time. There is no silent middle ground where malicious code executes undetected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating and Embedding SRI Hashes in Your Build Process
&lt;/h2&gt;

&lt;p&gt;When using Next.js 16.2 or later with Turbopack, SRI hash generation is built into the build pipeline. However, understanding the process helps you debug issues and ensure correctness.&lt;/p&gt;

&lt;p&gt;The build process proceeds in stages. First, Turbopack analyzes your application code, resolves dependencies, and creates bundling plan. It then executes the bundling: concatenating code, applying transformations, and generating the final &lt;code&gt;.js&lt;/code&gt; files that users download. At this point, Turbopack computes a cryptographic hash over each bundle file. It encodes this hash in base64 and creates the integrity attribute.&lt;/p&gt;

&lt;p&gt;Turbopack then generates the HTML that will be served to users. For each &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag that references a JavaScript bundle, Turbopack inserts the integrity attribute alongside the src. The HTML is then written to disk and becomes part of your deployment artifact.&lt;/p&gt;

&lt;p&gt;The integrity attribute can be applied not only to your own bundles but also to external scripts that you load. If your Next.js application dynamically loads a third-party script, you can compute the SRI hash for that script and embed it in your code. For example:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/analytics.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;integrity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha384-ABC123...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crossOrigin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anonymous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;crossOrigin&lt;/code&gt; attribute must be set to &lt;code&gt;anonymous&lt;/code&gt; for SRI to work with cross-origin resources. This tells the browser to request the resource without sending cookies or authentication headers, which is necessary for CORS reasons.&lt;/p&gt;

&lt;p&gt;Computing the hash for external third-party scripts requires that you either download the script and compute the hash locally, or that the third-party provider publishes the SRI hash for you. Many popular CDNs and service providers now publish SRI hashes in their documentation. If they do not, you must assume responsibility for obtaining the hash through a secure channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration and Customization
&lt;/h2&gt;

&lt;p&gt;Next.js applications can customize SRI behavior through the &lt;code&gt;next.config.js&lt;/code&gt; file. The configuration options allow you to enable or disable SRI, specify which hashing algorithm to use, and control which resources receive integrity attributes.&lt;/p&gt;

&lt;p&gt;A minimal configuration that explicitly enables SRI looks like this:&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;// next.config.js&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="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;enabled&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;algorithm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha384&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;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;algorithm&lt;/code&gt; option accepts &lt;code&gt;sha256&lt;/code&gt;, &lt;code&gt;sha384&lt;/code&gt;, or &lt;code&gt;sha512&lt;/code&gt;. SHA-384 is recommended as a balance between security strength and hash size. SHA-512 produces longer hashes that marginally increase HTML file size; SHA-256 is weaker than SHA-384 and should only be used if you need compatibility with very old browsers.&lt;/p&gt;

&lt;p&gt;You can also exclude certain bundles from SRI protection if needed, though this should be rare. Typically you might exclude inline scripts or bundles that change frequently during development. The configuration option for this varies depending on your Next.js version, so consult the official Next.js documentation for your specific version.&lt;/p&gt;

&lt;p&gt;Once SRI is enabled, it applies automatically to all output bundles. You do not need to manually insert integrity attributes or modify your component code. The build system handles everything. When you run &lt;code&gt;next build&lt;/code&gt;, the output HTML files will contain all necessary integrity attributes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validating SRI Implementation in Production
&lt;/h2&gt;

&lt;p&gt;After deploying a Next.js application with SRI enabled, verify that the integrity attributes are present and correct. The simplest check is to fetch the HTML and search for &lt;code&gt;integrity=&lt;/code&gt; in the response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://yourapp.example.com | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; integrity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should return one or more lines showing script tags with integrity attributes. If the command returns nothing, SRI is not being applied, which indicates a configuration error.&lt;/p&gt;

&lt;p&gt;A more thorough validation involves computing the hash of downloaded bundles yourself and comparing them to the integrity attributes. For each script tag with an integrity attribute, extract the hash, download the bundle, and compute its hash using the same algorithm.&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="c"&gt;# Download the bundle&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://cdn.example.com/bundle-abc123.js &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; bundle.js

&lt;span class="c"&gt;# Compute SHA-384 hash&lt;/span&gt;
&lt;span class="nb"&gt;sha384sum &lt;/span&gt;bundle.js

&lt;span class="c"&gt;# Compare to the integrity attribute from the HTML&lt;/span&gt;
&lt;span class="c"&gt;# If they match (after base64 encoding), SRI is working correctly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hash you compute will be in hexadecimal format, but the SRI hash in the HTML is base64-encoded. You need to convert between formats or use a tool that handles the encoding automatically. Many online SRI hash generators can verify this for you, though in a production environment you should automate this check as part of your deployment validation pipeline.&lt;/p&gt;

&lt;p&gt;Beyond manual verification, monitor your application's error logs and browser console for SRI failures. When an integrity check fails, the browser logs a clear error message. If you see these errors, it indicates that a bundle was modified during transit or on the server, which requires immediate investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Deployment Considerations
&lt;/h2&gt;

&lt;p&gt;When deploying a Next.js application with SRI enabled, several operational practices become important.&lt;/p&gt;

&lt;p&gt;First, ensure that your build process is deterministic. If the same source code produces different bundles each time you build, the hashes will change unpredictably and SRI verification will fail. Use the same build environment, Node.js version, and dependency versions for every build. Next.js and Turbopack are deterministic by design, but your custom build scripts or build plugins might not be.&lt;/p&gt;

&lt;p&gt;Second, coordinate your bundle deployment with HTML deployment. The integrity hashes are embedded in the HTML. If you deploy new bundles before deploying the new HTML, users will request bundles that do not exist, and their browsers will reject the old bundles due to hash mismatch. Both must be deployed as an atomic unit. Most deployment systems handle this naturally (you deploy a single artifact containing both HTML and bundles), but if your deployment process separates them, take care to update them together.&lt;/p&gt;

&lt;p&gt;Third, establish a process for updating external third-party scripts. If you load a third-party analytics script or widget with an SRI hash, and the third-party provider updates their script, your embedded hash becomes stale. The new script will have a different hash and your application will reject it. Subscribe to security bulletins and changelogs from third-party providers, and establish a process to update these hashes regularly. Some providers make this easier by publishing hashes in their documentation or by providing an API to query the current hash.&lt;/p&gt;

&lt;p&gt;Fourth, maintain a comprehensive inventory of all SRI hashes in your application, including those for external scripts. Document which scripts are protected, which algorithm is used, and where the hash came from (vendor documentation, computed locally, etc.). This inventory becomes a reference when debugging SRI failures and when updating hashes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Limitations and Complementary Measures
&lt;/h2&gt;

&lt;p&gt;SRI is powerful but operates within bounds. It cannot protect against compromises that occur before the bundle is signed. If your build server is compromised, an attacker can insert malicious code into your bundles and recompute the SRI hashes as part of the same attack. SRI protects against compromises of intermediaries (CDNs, caches, etc.) that do not have access to your private build infrastructure.&lt;/p&gt;

&lt;p&gt;SRI also cannot protect against compromises of your origin server if the attacker has the ability to update the HTML itself. If an attacker gains control of your web server and can modify both the JavaScript bundles and the HTML that references them, they can update both the integrity hashes and the bundles in lockstep, defeating SRI. However, this same level of compromise would allow them to inject malicious code without SRI anyway, so SRI does not worsen your security posture in this scenario.&lt;/p&gt;

&lt;p&gt;For comprehensive supply chain security, SRI must be combined with other practices. Code signing and verification of your build artifacts ensures that only authorized builds enter your deployment pipeline. Secure build infrastructure with restricted access and audit logging ensures that compromises of the build process are detected. Content Security Policy (CSP) headers restrict where scripts can be loaded from and what permissions they receive, providing a defense in depth even if SRI fails.&lt;/p&gt;

&lt;p&gt;A mature security posture combines all three: SRI for integrity verification of downloaded resources, CSP for runtime execution restrictions, and secure build practices to prevent compromise at the source. SRI addresses the supply chain between your infrastructure and users. The other measures address the supply chain before deployment and the runtime behavior after loading.&lt;/p&gt;




&lt;p&gt;For professional Web3 documentation or full-stack Next.js development work on your projects, visit &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>nextjs</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>TanStack and Next.js: The De Facto Frontend Logic Layer for 2026</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Tue, 14 Apr 2026 15:21:21 +0000</pubDate>
      <link>https://forem.com/mericcintosun/tanstack-and-nextjs-the-de-facto-frontend-logic-layer-for-2026-4mal</link>
      <guid>https://forem.com/mericcintosun/tanstack-and-nextjs-the-de-facto-frontend-logic-layer-for-2026-4mal</guid>
      <description>&lt;h2&gt;
  
  
  The Shift Toward Unified Data and Routing Logic
&lt;/h2&gt;

&lt;p&gt;The frontend architecture of production applications has undergone a fundamental realignment. Where developers once treated data fetching, caching, and routing as separate concerns managed by competing libraries, TanStack Query and TanStack Router have become the canonical pattern for managing application state in Next.js environments. This consolidation is not a matter of marketing momentum; it reflects a genuine convergence between the problems Next.js solved on the server and the problems that emerge when managing client-side logic in Server Components and Server Actions.&lt;/p&gt;

&lt;p&gt;The 2024-2026 period has solidified this pattern because the constraints of Server Components forced developers to think differently about where logic lives. Next.js Server Components run only on the server and cannot use browser APIs or React hooks directly. This pushed developers to reason explicitly about client boundaries and to separate concerns that were previously entangled in a single component tree. TanStack Query and TanStack Router emerged as the natural solution because they preserve React's declarative model while respecting the architectural constraints of the app router.&lt;/p&gt;

&lt;h2&gt;
  
  
  How TanStack Query Complements Server Components
&lt;/h2&gt;

&lt;p&gt;TanStack Query (formerly React Query) addresses a problem that Server Components do not fully solve: managing client-side state that derives from server data. When a Server Component fetches data, that data is rendered on the server and sent to the browser as HTML. If the user performs an action on the client that requires a fresh copy of that data, or if the data must be refetched asynchronously without a full page navigation, TanStack Query handles the mechanics.&lt;/p&gt;

&lt;p&gt;Server Components excel at initial data loading. The developer writes an async component, awaits the data, and renders it. The server delivers rendered HTML directly. TanStack Query takes over when the data must be kept in sync with server state after the initial load.&lt;/p&gt;

&lt;p&gt;Consider a dashboard that displays user metrics and allows the user to refresh those metrics without navigating. A Server Component loads the initial metrics and renders them. When the user clicks a refresh button on the client, that click handler is part of a Client Component nested inside or adjacent to the Server Component. A TanStack Query hook inside that Client Component manages the refetch request, caches the response, and updates the UI when new data arrives.&lt;/p&gt;

&lt;p&gt;The relationship between Server Components and TanStack Query is complementary, not redundant. Server Components handle the initial load. TanStack Query manages ongoing synchronization and client-side derivations of that data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integration with Server Actions
&lt;/h3&gt;

&lt;p&gt;The integration deepens when tRPC and Server Actions work together. A Server Action is a function marked with the &lt;code&gt;'use server'&lt;/code&gt; directive that runs on the server and can be called from Client Components. When a Server Action fetches or mutates data, the developer can invalidate TanStack Query caches to trigger a refetch.&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateUserName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&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;result&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&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;newName&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;revalidatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside a Client Component, a TanStack Query mutation calls this Server Action and handles cache invalidation:&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="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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useQueryClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;updateUserName&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/app/actions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserNameEditor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&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;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQueryClient&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;mutation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mutationFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;newName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;updateUserName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&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="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&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;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&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;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTarget&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;newName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
      &lt;span class="nx"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;Save&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/form&lt;/span&gt;&lt;span class="err"&gt;&amp;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;When the mutation succeeds, &lt;code&gt;invalidateQueries&lt;/code&gt; triggers a refetch of the data associated with that query key. TanStack Query handles the network request, loading states, and error handling. The Server Action carries out the database operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  TanStack Router and Client-Side Routing State
&lt;/h2&gt;

&lt;p&gt;TanStack Router complements TanStack Query by providing a declarative, type-safe router for Client Components. While Next.js App Router handles file-based routing and Server Components, TanStack Router manages the client-side routing state and search parameters with more granularity than Next.js's &lt;code&gt;useSearchParams&lt;/code&gt; hook.&lt;/p&gt;

&lt;p&gt;TanStack Router enables developers to parse, validate, and synchronize URL search parameters as strongly typed objects. This is especially valuable when building data-heavy interfaces that depend on filter state, pagination, or sorting criteria maintained in the URL.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RootRoute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rootRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RootRoute&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;usersRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;getParentRoute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;rootRoute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;validateSearch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routeTree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rootRoute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addChildren&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;usersRoute&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRouter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;routeTree&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the user navigates to &lt;code&gt;/users?page=2&amp;amp;sort=date&lt;/code&gt;, TanStack Router parses the search parameters, validates them against the schema, and provides them to the component as strongly typed values. The component can then use those values as keys for TanStack Query:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UsersPage&lt;/span&gt;&lt;span class="p"&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;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useSearch&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;isLoading&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;queryKey&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="s1"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Spinner&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UsersList&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="o"&gt;=&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="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;}
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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;When the user changes a filter or pagination value, TanStack Router updates the URL. This triggers a re-render and updates the query key, which causes TanStack Query to fetch new data. The URL becomes the source of truth for filtering and pagination state, and TanStack Query manages the data itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  tRPC: End-to-End Type Safety
&lt;/h2&gt;

&lt;p&gt;tRPC bridges the Next.js backend and frontend with full TypeScript support across the wire. Rather than defining REST endpoints and duplicating type definitions on the client, developers define procedures on the server and call them from the client with complete type inference.&lt;/p&gt;

&lt;p&gt;A tRPC backend router groups procedures into namespaces:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;initTRPC&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@trpc/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;initTRPC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appRouter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;router&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;users&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="nf"&gt;router&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;get&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="nx"&gt;procedure&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&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;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="p"&gt;}))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;span class="na"&gt;update&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="nx"&gt;procedure&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&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;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="p"&gt;}))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AppRouter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;appRouter&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the client, the tRPC client is instantiated with a link that points to the server endpoint. In Next.js with the app router, this often uses a fetch-based link that calls the tRPC endpoint during Server-Side Rendering or from a Client Component:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createTRPCClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;httpBatchLink&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@trpc/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/server/router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trpc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createTRPCClient&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AppRouter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;httpBatchLink&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000/api/trpc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&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;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When calling the procedure from the client, TypeScript knows the exact shape of the input and output:&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="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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// TypeScript infers that user has the shape of the User model&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;tRPC procedures can be called directly from Server Actions, bypassing the HTTP layer entirely. This pattern, called "direct invocation," avoids serialization overhead for server-to-server calls:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;appRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/server/router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&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;caller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;appRouter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCaller&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="c1"&gt;// context passed to all procedures&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&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;userId&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;When combined with TanStack Query, tRPC procedures become the mutation functions and query functions. The type safety flows from the procedure definition through the client code without intermediate steps:&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="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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trpc&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/trpc/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;queryKey&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="s1"&gt;users.get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;trpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&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;userId&lt;/span&gt; &lt;span class="p"&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;updateMutation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mutationFn&lt;/span&gt;&lt;span class="p"&gt;:&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;trpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mutate&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="na"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Invalidate and refetch&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&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;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;updateMutation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mutate&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;userId&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;Update&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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;h2&gt;
  
  
  State Synchronization Across Layers
&lt;/h2&gt;

&lt;p&gt;The challenge that TanStack Query and tRPC solve together is keeping client-side state synchronized with server state across multiple interaction patterns. A user can navigate, paginate, filter, mutate data, and expect the UI to reflect server state accurately without making redundant requests or displaying stale information.&lt;/p&gt;

&lt;p&gt;The synchronization strategy relies on query keys as the unit of cache invalidation. When a mutation succeeds, the developer invalidates the query key that corresponds to the affected data. TanStack Query then automatically refetches that data:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mutation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;mutationFn&lt;/span&gt;&lt;span class="p"&gt;:&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;trpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mutate&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="na"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;queryKey&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="s1"&gt;posts.list&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;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern scales because the cache keys encode the parameters that define the data. A query for &lt;code&gt;['posts', { userId: '1', page: 1 }]&lt;/code&gt; is distinct from &lt;code&gt;['posts', { userId: '2', page: 1 }]&lt;/code&gt;. When the user creates a post in one view and the query is invalidated, only the affected cache entries are refetched.&lt;/p&gt;

&lt;p&gt;In scenarios where the server-side state changes due to another user's action, TanStack Query supports polling and real-time subscriptions. The &lt;code&gt;refetchInterval&lt;/code&gt; option refetches data at a specified interval. For more sophisticated patterns, developers integrate libraries like Socket.io to push state changes to the client, triggering manual cache invalidation:&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="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;io&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post:created&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="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&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="s1"&gt;posts.list&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;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&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;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Handling Error States and Optimistic Updates
&lt;/h2&gt;

&lt;p&gt;A mature implementation of TanStack Query with tRPC addresses error handling and optimistic updates. Optimistic updates allow the UI to reflect changes immediately while the server request is in flight. If the request fails, the UI reverts to the previous state.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mutation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;mutationFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;trpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newData&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;onMutate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancelQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&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="s1"&gt;items&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;previousData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getQueryData&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setQueryData&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;items&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="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;old&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;span class="nx"&gt;newData&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;previousData&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setQueryData&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;previousData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&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="s1"&gt;items&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;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;onMutate&lt;/code&gt; hook runs before the request is sent. It saves the current cache state and updates the cache optimistically. If the mutation succeeds, &lt;code&gt;onSuccess&lt;/code&gt; refetches to ensure consistency. If it fails, &lt;code&gt;onError&lt;/code&gt; restores the previous state. This pattern provides responsive UI without sacrificing data accuracy.&lt;/p&gt;

&lt;p&gt;Error handling within tRPC procedures relies on throwing structured errors that the client can react to. tRPC provides a &lt;code&gt;TRPCError&lt;/code&gt; class that accepts a code and message:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appRouter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;router&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;items&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="nf"&gt;router&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;update&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="nx"&gt;procedure&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&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;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;data&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;object&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;span class="nf"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;item&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TRPCError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NOT_FOUND&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Item not found&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&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="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;On the client, the mutation's error callback receives the error object with the code and message:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mutation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;mutationFn&lt;/span&gt;&lt;span class="p"&gt;:&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;trpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mutate&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="na"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;:&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &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;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NOT_FOUND&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;showNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Item not found&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="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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;showNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;An error occurred&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="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="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;h2&gt;
  
  
  Practical Patterns for Large Applications
&lt;/h2&gt;

&lt;p&gt;In large applications, the modular structure of TanStack and tRPC becomes essential. Query keys should be organized hierarchically to avoid collisions and to make cache invalidation patterns clear. A common approach uses key factories:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;postsKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;all&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="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;lists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;postsKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PostFilters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;postsKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lists&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;postsKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;detail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;postsKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;details&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&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="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;postsKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;page&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="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;trpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;page&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="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onSuccess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;postsKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lists&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;By invalidating &lt;code&gt;postsKeys.lists()&lt;/code&gt;, all list queries are refetched regardless of their filter parameters. This avoids manually tracking which specific queries were affected.&lt;/p&gt;

&lt;p&gt;Router integration follows a similar pattern. Define routes at the top level, compose them into a route tree, and use hooks like &lt;code&gt;Route.useSearch()&lt;/code&gt; and &lt;code&gt;Route.useNavigate()&lt;/code&gt; to read and modify routing state:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usersRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;getParentRoute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;rootRoute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;validateSearch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;UserFilters&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersPage&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UsersPage&lt;/span&gt;&lt;span class="p"&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;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;usersRoute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useSearch&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;navigate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNavigate&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;handleFilterChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newFilters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserFilters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newFilters&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;This pattern keeps the URL in sync with the UI state automatically. When the user changes a filter, the URL updates, which triggers the query key to change, which causes TanStack Query to fetch new data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Middleware and Request Customization
&lt;/h2&gt;

&lt;p&gt;TanStack Query supports middleware to intercept and customize requests globally. Common use cases include adding authentication headers, logging, and retry logic. The &lt;code&gt;httpBatchLink&lt;/code&gt; in tRPC accepts a fetch function that developers can customize:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trpc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createTRPCClient&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AppRouter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;httpBatchLink&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000/api/trpc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getAuthToken&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;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&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;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;headers&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;redirectToLogin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&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;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TanStack Query's built-in retry mechanism handles transient failures automatically. By default, Query retries failed requests three times with exponential backoff. This behavior can be customized per query or globally:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;defaultOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;retryDelay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attemptIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attemptIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30000&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;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Integration with Server-Rendered Data
&lt;/h2&gt;

&lt;p&gt;One architectural challenge in Next.js is populating TanStack Query cache with data already fetched on the server. Rather than fetching the same data twice (once on the server and again on the client), developers can hydrate the cache using &lt;code&gt;hydrate&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dehydrate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HydrationBoundary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;QueryClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&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;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryClient&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;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prefetchQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;queryKey&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="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&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="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;trpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;page&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="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HydrationBoundary&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;dehydrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PostList&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/HydrationBoundary&lt;/span&gt;&lt;span class="err"&gt;&amp;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;The &lt;code&gt;prefetchQuery&lt;/code&gt; method fetches the data on the server. The &lt;code&gt;dehydrate&lt;/code&gt; function serializes the cache state and sends it to the client. Inside the client component, the &lt;code&gt;HydrationBoundary&lt;/code&gt; wraps components that will use the cached data. When those components call &lt;code&gt;useQuery&lt;/code&gt; with the same query key, they receive the pre-fetched data immediately without making another request.&lt;/p&gt;

&lt;p&gt;This pattern is crucial for performance. It eliminates the waterfall effect where the server renders the HTML, the client hydrates, the client code mounts, and only then does the first data fetch occur. The data is already present when the page loads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring and DevTools
&lt;/h2&gt;

&lt;p&gt;TanStack Query and tRPC provide developer tools for debugging cache state and network requests. TanStack Query DevTools displays all active and inactive queries, their cache state, their age, and the ability to manually refetch or invalidate them. Installation is straightforward:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;QueryClientProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ReactQueryDevtools&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query-devtools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Providers&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&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;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;QueryClientProvider&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ReactQueryDevtools&lt;/span&gt; &lt;span class="nx"&gt;initialIsOpen&lt;/span&gt;&lt;span class="o"&gt;=&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="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/QueryClientProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;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;The devtools panel appears as a floating button in the bottom corner of the browser during development. Clicking it opens a panel that shows all queries, their status, cache age, and data payload. This is invaluable when debugging state synchronization issues.&lt;/p&gt;

&lt;p&gt;tRPC provides similar observability through the &lt;code&gt;loggerLink&lt;/code&gt;, which logs all procedure calls to the browser console:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;loggerLink&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@trpc/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trpc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createTRPCClient&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AppRouter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;loggerLink&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;httpBatchLink&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000/api/trpc&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;h2&gt;
  
  
  The Standard Has Crystallized
&lt;/h2&gt;

&lt;p&gt;By 2026, the combination of TanStack Query, TanStack Router, and tRPC has become the standard approach for managing client-side data and routing in Next.js applications. This is not because of any single breakthrough feature but because these libraries solve real problems that every application faces: caching, synchronization, type safety, and routing state management. They compose cleanly with Next.js Server Components and Server Actions, filling the gap left by the shift away from traditional client-side frameworks.&lt;/p&gt;

&lt;p&gt;Developers who adopt this stack early gain consistency across their codebase. Data fetching, caching, and mutation follow a uniform pattern. Routing state is declarative and type-safe. Network requests are fully typed from endpoint to client. The URL serves as a persistent source of truth for filtering and pagination. Errors are handled structurally. Optimistic updates provide responsive UX. These are not novel concepts, but the integration of these libraries into a cohesive system is the defining architectural pattern of 2026.&lt;/p&gt;

&lt;p&gt;For professional Web3 documentation or a full-stack Next.js web application, consider reaching out via my Fiverr profile at &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>frontend</category>
      <category>nextjs</category>
      <category>react</category>
    </item>
    <item>
      <title>Rapid Next.js MVP Development with Generative AI in the Edge Computing Era</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Sun, 12 Apr 2026 15:21:20 +0000</pubDate>
      <link>https://forem.com/mericcintosun/rapid-nextjs-mvp-development-with-generative-ai-in-the-edge-computing-era-1b0j</link>
      <guid>https://forem.com/mericcintosun/rapid-nextjs-mvp-development-with-generative-ai-in-the-edge-computing-era-1b0j</guid>
      <description>&lt;p&gt;The infrastructure for building production applications has fundamentally shifted. Five years ago, deploying a Next.js application meant provisioning servers, managing container orchestration, and navigating complex DevOps pipelines. Today, a developer can describe an application in natural language, receive production-ready code within seconds, and deploy it globally across edge networks with a single push. This transformation reflects two converging technological forces: the maturation of generative AI as a code generation engine, and the rise of edge computing platforms like Vercel's Edge Network that abstract away infrastructure complexity.&lt;/p&gt;

&lt;p&gt;The emergence of tools like Vercel v0 and Lovable represents a qualitative shift in how we build software. These are not simple code generation utilities. They are agentic systems that interpret design specifications and requirements in multiple modalities—text, images, screenshots—and produce full-stack Next.js applications that execute on globally distributed edge networks. The developer's role has evolved accordingly. Rather than writing procedural code line-by-line, senior engineers increasingly function as agentic workflow managers, architecting prompts, evaluating AI-generated outputs, integrating complex business logic, and orchestrating multi-step development processes that combine human judgment with machine generation.&lt;/p&gt;

&lt;p&gt;This article examines how generative AI tools create production-ready Next.js applications for edge deployment, the architectural patterns that emerge from this workflow, and the skill transition required for developers to remain effective in this new paradigm.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Edge Computing Foundation
&lt;/h2&gt;

&lt;p&gt;Edge computing has become the default deployment platform for modern web applications, not a niche optimization. Vercel Edge Network, Cloudflare Workers, AWS Lambda@Edge, and similar platforms execute code at geographically distributed points of presence—often at Continental-scale regions or even ISP-level locations. This geographic distribution provides near-zero latency for end users regardless of where they are located.&lt;/p&gt;

&lt;p&gt;The critical architectural distinction lies in how edge platforms differ from traditional compute. An edge function runs with hard constraints: maximum execution duration of 10 to 30 seconds depending on the provider, limited memory allocation (typically 128 MB to 3 GB), and stateless execution with no persistent local storage. These constraints are not bugs; they enforce architectural discipline. An application designed for edge execution cannot maintain internal state, cannot spawn long-running background processes, and must complete database queries and external API calls within strict timeframes.&lt;/p&gt;

&lt;p&gt;Next.js on edge platforms leverages incremental static regeneration (ISR) and streaming server components to optimize for these constraints. A typical edge-deployed Next.js application renders static or semi-static content at build time, serves it from a globally distributed CDN, and invokes server-side logic only when necessary. For dynamic content, the application uses Streaming Server Components that send HTML fragments to the client as data becomes available, rather than waiting for all data to load before rendering the first byte.&lt;/p&gt;

&lt;p&gt;Vercel's Edge Network integrates seamlessly with the Next.js framework because the platform and framework were built in concert. When you deploy a Next.js application to Vercel, API routes automatically execute on edge functions, middleware executes before route evaluation, and static assets are cached at edge locations worldwide. Database calls are automatically pooled and routed to the nearest database replica. Image optimization happens at edge locations closest to the client. This integration means developers do not explicitly orchestrate edge execution; the framework and platform handle it transparently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generative AI as Code Generation Engine
&lt;/h2&gt;

&lt;p&gt;Vercel v0 and Lovable occupy different positions within the same ecosystem, yet both follow an identical operational pattern: accept multimodal input, synthesize a complete application, and output deployable code.&lt;/p&gt;

&lt;p&gt;Vercel v0 ingests a design screenshot or text description and generates a React component or full page that matches the visual specification. The tool does not simply trace outlines or apply templates. Instead, it uses vision models to understand visual hierarchy, typography, color relationships, and spatial composition, then generates semantic HTML and Tailwind CSS that reproduces those properties. The output is not pixel-perfect to the input; rather, it is a faithful interpretation of the design intent. A developer can upload a Figma screenshot, receive a React component, and begin integrating it with backend logic immediately.&lt;/p&gt;

&lt;p&gt;Lovable extends this capability to full-stack applications. Given a natural language specification—a feature description, user story, or even a screenshot—Lovable generates a complete Next.js application including frontend components, API routes, database schemas, and configuration. The tool models the dependencies between frontend and backend, understanding that a form on the client requires a corresponding API endpoint on the server, which in turn requires database tables and validation logic.&lt;/p&gt;

&lt;p&gt;Both tools operate through iterative refinement loops. A developer generates an initial implementation, evaluates the output, and provides feedback. The generative system incorporates the feedback, re-synthesizes the affected components, and returns a revised version. This feedback loop continues until the developer accepts the output. This workflow is fundamentally different from traditional pair programming or code review. The human is not commenting on existing code; the human is shaping the specification that a generative system uses to produce new code.&lt;/p&gt;

&lt;p&gt;The quality of the generated code depends entirely on the quality of the input specification. Ambiguous requirements produce ambiguous code. Vague design directions result in generic implementations. The developer's responsibility is to provide precise, detailed specifications that constrain the generative system toward the desired output. A prompt that reads "make a todo list app" generates commodity code. A prompt that reads "create a todo list where items can be grouped by category, where categories persist to localStorage, and where users can drag items between categories to reorder them" produces something closer to the intended system.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Code Writer to Agentic Workflow Manager
&lt;/h2&gt;

&lt;p&gt;The developer's role has undergone a qualitative transformation. Ten years ago, developers were code writers. They received specifications and translated them into procedural logic. Five years ago, developers began to be architects, designing systems where other code could run. Today, effective developers are agentic workflow managers. They design the prompts and iterative processes that guide generative systems toward desired outputs, evaluate and critique machine-generated code, integrate AI-generated components into larger systems, and make architectural decisions about which parts of the system should be generated and which should be hand-written.&lt;/p&gt;

&lt;p&gt;This shift is not regression toward less technical work. It is an evolution toward more complex technical work. Consider a developer building an e-commerce platform. Previously, she would implement the product catalog schema, write the database queries, build the search API, and optimize the frontend rendering. Today, she writes a detailed specification of the product catalog requirements, generates the schema with an AI system, reviews and refines it, generates the API routes, tests them against edge cases, evaluates whether the generated code can scale to her data volume, and integrates it with a payment provider API that the generative system cannot generate.&lt;/p&gt;

&lt;p&gt;The skills that matter are no longer typing speed or knowledge of library APIs. They are clarity of specification, architectural judgment, and the ability to evaluate machine output critically. A weak developer can write poor code. A weak agentic workflow manager can write poor prompts and fail to recognize when generated code is insufficient. An effective agentic workflow manager can specify complex requirements precisely, generate code that matches those requirements, identify edge cases and failures in the generated output, and iteratively refine the specification until the output is correct.&lt;/p&gt;

&lt;p&gt;This role requires more technical depth, not less. To evaluate whether a generated database schema will perform adequately for your use case, you must understand indexing, query planning, and data distribution. To integrate a generated API with an external service, you must understand authentication protocols, error handling, and rate limiting. To architect a system using generative code, you must design the interfaces between components, understand failure modes, and make tradeoffs between completeness and simplicity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architectural Patterns for AI-Generated Applications
&lt;/h2&gt;

&lt;p&gt;Applications generated by tools like Vercel v0 and Lovable follow consistent architectural patterns because the generative models have learned these patterns from the training data.&lt;/p&gt;

&lt;p&gt;The first pattern is component-driven architecture. Rather than building monolithic pages, generated applications are organized as composable React components with clear boundaries. A component receives data as props, manages internal state with hooks, and communicates with parent components through callbacks. This architecture emerges not because the generative system was explicitly instructed to follow it, but because the underlying language models have learned that this is how well-maintained Next.js applications are structured.&lt;/p&gt;

&lt;p&gt;The second pattern is API-driven backend architecture. Server-side logic is encapsulated in API routes, not embedded in page components. A component calls an API endpoint, receives JSON, and renders the response. This separation enables reuse of backend logic across multiple frontend routes, enables easier testing, and enables deployment of frontend and backend independently if needed. Generated applications typically expose a RESTful API, though this is a convention that can be overridden through specification.&lt;/p&gt;

&lt;p&gt;The third pattern is static-first with dynamic fallback. Generated applications render static content whenever possible, using incremental static regeneration to update content on a schedule. Dynamic content is fetched client-side when necessary. This pattern is optimal for edge deployment because it minimizes compute consumption and reduces latency for repeat visitors.&lt;/p&gt;

&lt;p&gt;The fourth pattern is minimal state management. Rather than implementing Redux, Zustand, or other state management libraries, generated applications typically use React's built-in state management with hooks. Server state and client state are kept separate. This simplicity is a strength; it reduces cognitive load and reduces opportunities for state management bugs.&lt;/p&gt;

&lt;p&gt;These patterns are not mandates; they emerge because they are consistent with how the training data was structured. A developer who understands these patterns can evaluate generated code quickly, identify when deviations might be necessary, and guide the generative system toward architectures that are better suited to specific requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vercel v0: Design-to-Component Translation
&lt;/h2&gt;

&lt;p&gt;Vercel v0 solves a specific, well-scoped problem: translate visual designs into React components. The workflow is straightforward. A designer creates a mockup in Figma, Adobe XD, or any visual design tool. The developer takes a screenshot of the design, uploads it to v0, and receives a React component that visually matches the screenshot. The component is written in modern React with Tailwind CSS for styling, is fully responsive, and includes proper accessibility attributes.&lt;/p&gt;

&lt;p&gt;The power of v0 emerges from understanding what it does well and what it does not do. It excels at structural translation: taking a visual hierarchy and producing semantic HTML that preserves that hierarchy. It handles typography, spacing, color, and grid-based layouts reliably. It produces responsive designs that adapt to different screen sizes without explicit media queries.&lt;/p&gt;

&lt;p&gt;Where v0 reaches its limitations is interactivity and behavior. A screenshot contains no information about what happens when a user clicks a button. v0 can generate an onClick handler, but the handler itself must be specified by the developer. Similarly, v0 cannot know what data should flow through a component, so components are generated with placeholder data. A developer receives a beautiful component with an empty state, and must then integrate it with actual data sources.&lt;/p&gt;

&lt;p&gt;The effective workflow is generation followed by integration. A developer generates a component from a design, reviews it for visual correctness, then integrates it with props, state management, and data sources. This workflow is dramatically faster than writing components by hand, because the developer is not deciding every spacing value and color; she is only adding behavior and data integration.&lt;/p&gt;

&lt;p&gt;v0 components are intentionally unstyled beyond layout and visual properties. A generated button component includes click handlers (though empty), but the button styling comes from Tailwind. This choice allows developers to apply consistent design systems across generated components without re-generating everything. A developer can change the primary button color across an entire application by modifying a Tailwind config, not by regenerating every component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lovable: Full-Stack Application Generation
&lt;/h2&gt;

&lt;p&gt;Lovable extends design-to-code translation into full-stack generation. A developer provides a specification—often a design, a screenshot, or a detailed feature description—and Lovable generates a complete Next.js application with frontend, API routes, and database schema.&lt;/p&gt;

&lt;p&gt;The specification process is critical. Unlike v0, which can infer structure from a visual design, Lovable requires explicit requirements about functionality. A specification might read: "Create a task management application where users can create, edit, and delete tasks. Tasks have a title, description, due date, and status. Users can filter tasks by status and sort by due date. The application should persist data to a PostgreSQL database."&lt;/p&gt;

&lt;p&gt;Lovable synthesizes several components from this specification. It generates React components for the task list view, task detail view, and task creation form. It generates API routes for creating, reading, updating, and deleting tasks (standard CRUD operations). It generates a database schema with a tasks table and appropriate indexes. It generates client-side code to communicate with the API. It generates validation logic on both client and server. It generates error handling and loading states.&lt;/p&gt;

&lt;p&gt;The generated code is production-adjacent, meaning it can often be deployed immediately, but more commonly requires integration work. A developer receives a functional application that executes the specified requirements, but the application likely lacks sophisticated error handling, advanced features, external API integrations, or optimizations. The developer's role is to evaluate the generated code, identify gaps, enhance critical sections, and integrate with external systems.&lt;/p&gt;

&lt;p&gt;A key architectural decision that Lovable makes is persistence mechanism. When generating a new application, Lovable typically targets PostgreSQL with a client library like &lt;code&gt;pg&lt;/code&gt; or an ORM like Prisma. This choice is reasonable for new applications because PostgreSQL is reliable, widely available, and suitable for most use cases. However, an application with different requirements—perhaps needing real-time synchronization, or needing to work offline—might be better served by a different database system. A developer who wants to use Firebase, MongoDB, or another database must either regenerate the application with an explicit requirement, or hand-modify the generated code to use the desired system.&lt;/p&gt;

&lt;p&gt;Lovable operates in iterative generation cycles. A developer generates an initial application, evaluates it, identifies missing features or incorrect behavior, and provides feedback. Lovable regenerates the affected components based on the feedback. This cycle continues until the developer accepts the output. Each iteration should bring the application closer to the intended specification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge Deployment and Optimization
&lt;/h2&gt;

&lt;p&gt;Deploying AI-generated Next.js applications to edge platforms like Vercel involves understanding a few architectural constraints and optimization strategies.&lt;/p&gt;

&lt;p&gt;Generated applications are typically server-side rendered or use incremental static regeneration. Server components execute on edge functions, which means database calls, external API calls, and all server-side logic execute on edge functions. This is generally efficient because edge functions colocate computation with the user, minimizing latency. However, a developer must be aware that edge function execution is time-limited. If an API route makes three sequential external API calls that take five seconds each, the total execution time is fifteen seconds, which exceeds the edge function timeout on many platforms.&lt;/p&gt;

&lt;p&gt;Optimization requires understanding the chain of dependencies. If an edge function must fetch data from a database before rendering, it should use connection pooling (which Vercel's Postgres integration provides automatically) to avoid connection overhead. If an edge function must call an external API, it should make the call in parallel with other calls, not sequentially. If the external API is slow or unreliable, the edge function should implement timeout and retry logic.&lt;/p&gt;

&lt;p&gt;Generated applications often include unnecessary data fetching. A developer might specify "fetch the user's profile, their recent tasks, and task statistics" as a single API call. If fetching these three pieces of data requires three separate database queries, the edge function executes three queries serially, which multiplies latency. An optimized version batches these queries or restructures the data fetching to run in parallel.&lt;/p&gt;

&lt;p&gt;Cache headers are essential for edge deployment. Static content should be marked with long-lived cache headers so that edge nodes cache the content and serve repeat visitors from cache. Dynamic content should be marked with appropriate cache headers that communicate to edge nodes how long the content remains fresh. ISR (Incremental Static Regeneration) depends on cache headers; a page is cached at edge nodes, and when the cache expires, Next.js regenerates the page and updates the cache.&lt;/p&gt;

&lt;p&gt;Generated applications often lack sophisticated caching strategies because generative systems tend toward simplicity. A developer should evaluate the generated cache headers and adjust them based on the application's actual refresh requirements. A dashboard that displays real-time data should not cache responses. A product catalog that updates once daily can safely cache responses for hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Critical Evaluation of Generated Code
&lt;/h2&gt;

&lt;p&gt;Generated code is not automatically production-ready. It is often foundation-ready; it is correct, reasonably efficient, and handles common cases well, but it lacks defensive programming, edge case handling, and optimizations necessary for production.&lt;/p&gt;

&lt;p&gt;A developer evaluating generated code should ask several questions. First: does the code handle errors? Generated APIs often lack error handling for database failures, network timeouts, or validation failures. A production API must return appropriate error responses and log errors for debugging. Second: does the code validate inputs? Generated forms may validate on the client but lack server-side validation. A malicious client can bypass client-side validation, so server-side validation is essential. Third: does the code handle edge cases? Generated code handles the happy path well. If a user tries to create a task with an empty title, or tries to delete a task they do not own, the generated code may fail ungracefully.&lt;/p&gt;

&lt;p&gt;Fourth: does the code scale? Generated code often executes fine for hundreds of records but may perform poorly for millions. A developer should understand the database schema, query patterns, and indexes to evaluate whether the generated code scales to the expected data volume. Fifth: does the code follow the application's conventions? Generated code may not match existing patterns in the codebase, and integrating generated code alongside hand-written code may introduce inconsistency.&lt;/p&gt;

&lt;p&gt;The most important evaluation question is whether the generated code actually implements the specification correctly. Generative systems sometimes misinterpret requirements, implement incomplete features, or produce code that technically works but not in the way the developer intended. A developer must test the generated code thoroughly against the specification and provide feedback when the implementation diverges.&lt;/p&gt;

&lt;p&gt;Testing generated code is crucial. Automated tests for API routes, React components, and database operations should pass before the code is deployed. Generated tests are often generic and may not cover the specific behavior required by the application. A developer should write additional tests for critical functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration Workflows: Generated and Hand-Written Code
&lt;/h2&gt;

&lt;p&gt;Real applications combine generated code with hand-written code. A developer might generate an API route for a standard operation like fetching a user's profile, then hand-write a more complex route for processing payments or sending notifications.&lt;/p&gt;

&lt;p&gt;The integration strategy matters. Generated components should be designed to receive data through props, rather than fetching data directly. This pattern separates data fetching (which might be hand-written or generated) from presentation (which might be hand-written or generated). A hand-written page component can fetch data and pass it to a generated component, or a generated page component can fetch data and pass it to a hand-written component.&lt;/p&gt;

&lt;p&gt;API route integration requires similar discipline. Hand-written API routes should follow the same patterns as generated routes: accept request data, validate it, execute business logic, and return JSON responses. A hand-written route that implements a different pattern (for example, returning HTML instead of JSON) creates confusion and makes integration harder.&lt;/p&gt;

&lt;p&gt;Database schema integration requires the most care. If a generated schema exists, hand-written code must respect that schema's structure. If a developer needs to modify the schema—adding columns, creating indexes, changing types—the generated API routes may need corresponding updates. Tools like Prisma can help manage schema versioning and generate migrations automatically, but a developer must verify that generated code remains compatible with schema changes.&lt;/p&gt;

&lt;p&gt;A practical approach is to use generated code as a starting point, evaluate it, enhance it where necessary, and then treat it as hand-written code. Rather than regenerating the entire API, a developer might regenerate a single component and merge it into the existing codebase. This hybrid approach combines the speed of generation with the precision of hand-writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt Engineering and Specification
&lt;/h2&gt;

&lt;p&gt;The quality of generated code depends almost entirely on the quality of the specification. A vague prompt produces vague code. A precise specification produces precise code.&lt;/p&gt;

&lt;p&gt;Effective specifications are specific about requirements, not about implementation. Rather than "use React hooks," a specification should describe what state needs to be managed. Rather than "make it responsive," a specification should describe how the layout should adapt to different screen sizes. Rather than "validate the email field," a specification should describe what constitutes a valid email in the context of the application.&lt;/p&gt;

&lt;p&gt;Specifications should include examples. "Create a form with first name, last name, and email fields" is less useful than "Create a sign-up form. The form should have text inputs for first name, last name, and email. When submitted, the form should send the data to /api/signup. If the request succeeds, redirect to the login page. If the request fails, show the error message to the user."&lt;/p&gt;

&lt;p&gt;Specifications should clarify data sources and API contracts. Rather than "fetch user data," specify "fetch the user's profile from GET /api/user/{userId}. The response will be JSON with name, email, and profile picture URL."&lt;/p&gt;

&lt;p&gt;Specifications should include constraints. "The application should support up to 10,000 users" communicates different requirements than "The application is a personal project for 10 friends." "The form must work with JavaScript disabled" is a concrete constraint that affects implementation.&lt;/p&gt;

&lt;p&gt;Iterative refinement is normal. A developer generates code from an initial specification, evaluates it, identifies gaps or misunderstandings, and provides feedback. "The generated form doesn't show an error message when the email is already registered" is feedback that clarifies the specification. The developer regenerates the form with the clarification, and the output improves.&lt;/p&gt;

&lt;p&gt;The developer's prompt engineering skill directly impacts the quality of the generated code. Developers who write clear, specific, example-rich specifications receive better code than developers who write vague specifications. This is not mysterious. The generative system is trying to interpret human intent, and clearer intent produces better interpretations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-Time Collaboration and Feedback
&lt;/h2&gt;

&lt;p&gt;Modern generative AI tools support real-time collaboration where multiple developers and designers can provide feedback and guide the generation process. This workflow is fundamentally different from traditional code review, where reviewers comment on completed code.&lt;/p&gt;

&lt;p&gt;In real-time generative workflows, a developer might draft a specification, pass it to the generative system, and receive a component. A designer evaluates the component visually and provides feedback: "the padding is too tight" or "the color doesn't match the design system." A backend developer provides feedback: "the API calls are happening sequentially; they should happen in parallel." A QA engineer provides feedback: "the form doesn't handle the case where the database is unavailable."&lt;/p&gt;

&lt;p&gt;The generative system integrates all feedback and produces a revised component. This cycle continues until all stakeholders accept the output. This workflow distributes the responsibility for correctness across the team, rather than concentrating it in a single person's hands.&lt;/p&gt;

&lt;p&gt;Tools like Lovable and v0 support this collaboration through shared projects and version history. Multiple team members can view the same project, see the generated code, and contribute feedback. Version history allows rollback if a generation introduces a regression.&lt;/p&gt;

&lt;p&gt;This collaborative workflow requires clear communication. Team members must explain their feedback precisely. "This looks wrong" is unhelpful. "When the user submits the form with an empty title, the application crashes. The error message should be 'Title is required'" is clear feedback that the generative system can incorporate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and When to Hand-Write
&lt;/h2&gt;

&lt;p&gt;Generative AI tools excel at generating code that follows established patterns and conventions. They struggle with code that requires deep domain knowledge, complex business logic, or novel solutions. Understanding these limitations is critical for deciding when to generate and when to hand-write.&lt;/p&gt;

&lt;p&gt;Generated code excels at CRUD (create, read, update, delete) operations, form handling, data validation, and standard API patterns. A generated todo list application is likely to be correct and deployable. Generated code struggles with complex algorithms, optimization problems, and domain-specific logic. A generated recommendation engine is unlikely to be sophisticated enough for production.&lt;/p&gt;

&lt;p&gt;Generated code also struggles with integrations to external systems that require custom authentication, complex error handling, or nuanced behavior. An integration to a payment processor, for example, typically requires careful error handling, retry logic, and security considerations that generative systems may not fully capture.&lt;/p&gt;

&lt;p&gt;Generated code is weak on performance optimization. A generative system might produce code that works but has N+1 query problems, missing indexes, or inefficient algorithms. A developer should evaluate generated code for performance issues and optimize manually if necessary.&lt;/p&gt;

&lt;p&gt;Generated code typically lacks sophisticated security measures. It validates inputs and authenticates users, but it may not implement rate limiting, CSRF protection, or other defensive security measures. Critical security-sensitive features should be hand-written and reviewed by security experts.&lt;/p&gt;

&lt;p&gt;The practical approach is a hybrid: generate what the generative system does well, hand-write what requires domain expertise or custom behavior, and integrate both seamlessly. This approach combines the speed of generation with the precision of hand-writing, producing applications that are both fast to build and high-quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Developer Role in an Agentic Ecosystem
&lt;/h2&gt;

&lt;p&gt;The emergence of agentic code generation does not eliminate the need for developers. It transforms the role. Rather than writing code, developers orchestrate the generation of code. Rather than implementing features, developers specify features and evaluate implementations.&lt;/p&gt;

&lt;p&gt;This transformation requires different skills than traditional development. The ability to write clear specifications is more important than the ability to remember API syntax. The ability to evaluate code critically is more important than typing speed. The ability to understand system architecture is more important than knowledge of library internals.&lt;/p&gt;

&lt;p&gt;Effective developers in this ecosystem combine technical depth with clear communication. They understand how systems work at a fundamental level so they can evaluate whether generated code is correct. They understand how to specify requirements clearly so the generative system produces the right output. They understand how to integrate generated code with hand-written code so the system functions coherently.&lt;/p&gt;

&lt;p&gt;The most valuable developers will be those who can switch fluidly between roles: sometimes generating code quickly to validate ideas, sometimes hand-writing critical components, sometimes reviewing generated code and providing feedback. This flexibility requires mastery of the underlying technologies, not just the ability to use a generative tool.&lt;/p&gt;

&lt;p&gt;Developers who resist generative tools or dismiss them as inferior to hand-written code will find themselves less competitive. Developers who learn to use them effectively will ship significantly faster and focus their time on problems that require human expertise rather than routine implementation work.&lt;/p&gt;




&lt;p&gt;If you need professional Web3 documentation or a complete full-stack Next.js application, visit &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cloudcomputing</category>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Advanced Cache Management in Next.js 16: updateTag and revalidateTag</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Fri, 10 Apr 2026 15:21:21 +0000</pubDate>
      <link>https://forem.com/mericcintosun/advanced-cache-management-in-nextjs-16-updatetag-and-revalidatetag-50j2</link>
      <guid>https://forem.com/mericcintosun/advanced-cache-management-in-nextjs-16-updatetag-and-revalidatetag-50j2</guid>
      <description>&lt;p&gt;Next.js 16 introduced two distinct cache invalidation functions that address a critical gap in data-heavy applications: keeping user-facing content fresh without triggering unnecessary recomputation. The distinction between &lt;strong&gt;updateTag&lt;/strong&gt; and &lt;strong&gt;revalidateTag&lt;/strong&gt; is not merely semantic. Understanding when to apply each function determines whether your application maintains data consistency under load or wastefully recomputes entire datasets.&lt;/p&gt;

&lt;p&gt;This article examines the technical mechanics of both approaches, explains the resource efficiency implications, and provides production-grade code patterns for managing cache in real-world scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cache Problem in Data-Intensive Applications
&lt;/h2&gt;

&lt;p&gt;Before Next.js 16, developers built cache invalidation on binary choices: either invalidate an entire tag and force a recompute, or leave stale data in circulation. For applications serving product catalogs with thousands of SKUs, user feeds with constantly updating content, or financial dashboards where seconds matter, this binary approach creates two failure modes.&lt;/p&gt;

&lt;p&gt;The first failure occurs when recomputation is too expensive. A complete feed rebuild might take 30 seconds and consume significant database resources. If that happens on every cache tag invalidation, users experience cascading slowdowns as invalidation requests queue up behind expensive rebuilds.&lt;/p&gt;

&lt;p&gt;The second failure occurs when recomputation is skipped entirely. Developers cache aggressively to avoid the first problem, but stale data propagates. A user updates their profile, the cache still serves the old version, and the user sees a broken experience.&lt;/p&gt;

&lt;p&gt;Next.js 16 resolves this tension by separating concerns: &lt;strong&gt;updateTag&lt;/strong&gt; refreshes cached content incrementally while &lt;strong&gt;revalidateTag&lt;/strong&gt; performs full reconstruction when necessary. The key insight is that not every data change requires full recomputation.&lt;/p&gt;

&lt;h2&gt;
  
  
  How updateTag Works
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;updateTag&lt;/strong&gt; function updates tagged cache entries in place without triggering a rebuild of the entire route or component. It accepts the tag identifier and a new value, then surgically replaces what the cache holds.&lt;/p&gt;

&lt;p&gt;Here is how it integrates into a typical Server Action:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;updateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateUserProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserProfile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Update database&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updated&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Update the specific cache entry&lt;/span&gt;
  &lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-profile-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caller tags cached data with a unique key like &lt;code&gt;user-profile-${userId}&lt;/code&gt;. When the user updates their profile, &lt;strong&gt;updateTag&lt;/strong&gt; replaces the cached object with the new one. The Next.js routing layer does not re-render the page. The cache simply swaps the stale value for the fresh one.&lt;/p&gt;

&lt;p&gt;Resource efficiency flows from this mechanism. If a user's profile cache holds a serialized JSON object of 50 KB, &lt;strong&gt;updateTag&lt;/strong&gt; replaces those 50 KB instantly. The database query already executed (it had to, to fetch the new data), but we skip the rendering step, any downstream API calls that might be needed to populate the page, and the full cache rewrite that would occur with &lt;strong&gt;revalidateTag&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  When updateTag Applies
&lt;/h3&gt;

&lt;p&gt;Use &lt;strong&gt;updateTag&lt;/strong&gt; when the update targets a single, well-defined object or collection that does not cascade to dependent caches. The pattern works for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User profile fields (name, email, avatar)&lt;/li&gt;
&lt;li&gt;Product metadata (price, stock count, description)&lt;/li&gt;
&lt;li&gt;Comment updates or deletions&lt;/li&gt;
&lt;li&gt;Inventory adjustments for a single item&lt;/li&gt;
&lt;li&gt;Settings or configuration changes scoped to a single resource&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shared property is that the change is local. Updating a user's email address does not invalidate the homepage feed or product listing page. The scope is bounded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resource Implications of updateTag
&lt;/h3&gt;

&lt;p&gt;Let's measure the concrete difference. Assume a product detail page for an e-commerce site:&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;// pages/products/[id].tsx&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProductWithReviews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&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;product&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;productId&lt;/span&gt; &lt;span class="p"&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;reviews&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;take&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reviewCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;averageRating&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;calculateAverage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reviews&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;span class="c1"&gt;// In the 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="k"&gt;async&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="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&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="kr"&gt;string&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;productData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getProductWithReviews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;productData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;productData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Rating&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;productData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;averageRating&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/5&amp;lt;/&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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;When a product price changes, &lt;strong&gt;revalidateTag&lt;/strong&gt; would re-execute &lt;code&gt;getProductWithReviews&lt;/code&gt;, hitting the database twice (once for the product, once for reviews), calculating the average rating again, and re-rendering the entire page. Even if only the price changed, the function reruns in full.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;updateTag&lt;/strong&gt;, we target only the price:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;updateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateProductPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;productId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newPrice&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Surgical update: only refresh the price field in the cache&lt;/span&gt;
  &lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`product-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newPrice&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;The cached product object updates in microseconds. No database queries fire beyond the update itself. No re-rendering occurs. The page serves fresh data on the next request without the computational overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  How revalidateTag Works
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;revalidateTag&lt;/strong&gt; function invalidates an entire cache tag and forces a full recompute on the next request. When called, it purges all cached data associated with that tag. The application then treats the next incoming request for that resource as a cache miss and executes the full data-fetching and rendering pipeline.&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revalidateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deleteComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;articleId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;commentId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Full recompute the article and its comment section&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`article-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;articleId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;When &lt;strong&gt;revalidateTag&lt;/strong&gt; executes, Next.js marks the tag as stale. On the next user request to the article, the application re-fetches the article content, re-fetches the comments, recalculates derived data (comment count, newest comment timestamp), and re-renders the page.&lt;/p&gt;

&lt;h3&gt;
  
  
  When revalidateTag Applies
&lt;/h3&gt;

&lt;p&gt;Use &lt;strong&gt;revalidateTag&lt;/strong&gt; when the change cascades across multiple dependent data sources or when the affected data structure is complex enough that surgical updates become unmaintainable. Common scenarios include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deleting a resource that appears in multiple places (a user deletion affects their profile page, their comments, their posts)&lt;/li&gt;
&lt;li&gt;Publishing content that triggers derived calculations (publishing an article invalidates homepage recommendations)&lt;/li&gt;
&lt;li&gt;Changing data that other cached queries depend on (updating a category affects product listings, homepage sections, and filter options)&lt;/li&gt;
&lt;li&gt;Bulk operations where identifying every affected cache entry is impractical&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is clear: &lt;strong&gt;revalidateTag&lt;/strong&gt; guarantees correctness at the cost of computational expense. When a user deletes their account, we invalidate the user profile tag, the user's posts tag, the user's comments tag, and any feed that includes their content. Full recomputation ensures no orphaned or inconsistent data remains in the cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resource Implications of revalidateTag
&lt;/h3&gt;

&lt;p&gt;Consider a blogging platform where users can publish articles:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revalidateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;publishArticle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;articleId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&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;article&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;articleId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;published&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;publishedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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;span class="c1"&gt;// Article publishing cascades&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;homepage-featured&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-authored-posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category-articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an article publishes, every cached page that might include it becomes invalid. The homepage's featured section recomputes. The user's authored posts list recomputes. Category article listings recompute. On the next requests to these pages, the application executes their full data-fetching logic.&lt;/p&gt;

&lt;p&gt;This is the correct behavior. A new published article must appear everywhere it belongs. No surgical update can achieve this because the change affects multiple independent cache entries.&lt;/p&gt;

&lt;p&gt;However, the cost is measurable. If the homepage query aggregates 50 articles, recomputes recommendations based on 10,000 user interactions, and sorts by engagement, that recompute might take 2-5 seconds. If articles publish frequently, the recomputation queue fills up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distinguishing Between updateTag and revalidateTag
&lt;/h2&gt;

&lt;p&gt;The decision between these functions hinges on scope and correctness. Build a mental model using two questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First question: Can the change be expressed as a partial update to the existing cached object?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If yes, &lt;strong&gt;updateTag&lt;/strong&gt; applies. If the change requires merging new data from the database or recalculating derived fields, &lt;strong&gt;revalidateTag&lt;/strong&gt; is safer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second question: Does the change affect any cached data outside the immediate resource?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If no, &lt;strong&gt;updateTag&lt;/strong&gt; is the right choice. If yes, &lt;strong&gt;revalidateTag&lt;/strong&gt; avoids inconsistency.&lt;/p&gt;

&lt;p&gt;Let's apply this to specific scenarios:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario 1: User Updates Their Bio&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A user changes their bio from "Software engineer" to "Senior software engineer." Can we express this as a partial update to the cached user profile object? Yes. Does this change affect other cached data like user search results or team member listings? Probably not; the search index and team listing were last computed minutes ago and are not strictly dependent on bio text. Use &lt;strong&gt;updateTag&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;updateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateUserBio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newBio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&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;updated&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newBio&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-profile-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bio&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;&lt;strong&gt;Scenario 2: User Follows Another User&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A user clicks follow on another user's profile. This action adds a follower to the target user's follower count and adds a following to the source user's following count. These derived fields exist across multiple cached pages: the follower list, the user profile, and the social graph. Can we express this as a simple field update? Technically yes, but we would need to update two separate cache entries and ensure atomicity. Use &lt;strong&gt;revalidateTag&lt;/strong&gt; to guarantee consistency.&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revalidateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;followUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;followerUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;follows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;followerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;followerUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;followingId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;targetUserId&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;span class="c1"&gt;// Both users' social graphs change&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-social-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;followerUserId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-social-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;targetUserId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;&lt;strong&gt;Scenario 3: Updating Product Stock&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A product's stock count decreases by 1 because a customer purchased it. This is a simple numeric decrement on a single field. The change does not cascade to other products or listings. Can we surgically update the cache? Absolutely. Use &lt;strong&gt;updateTag&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;updateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decrementProductStock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&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;product&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;productId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;decrement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;quantity&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;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`product-stock-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lastUpdated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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;&lt;strong&gt;Scenario 4: Changing a Product Category&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A product moves from the "Electronics" category to the "Computing" category. This change affects the product detail page cache, the Electronics category listing cache, and the Computing category listing cache. Multiple independent cache entries reference this product. A surgical update to only the product's cache would leave the category listings stale. Use &lt;strong&gt;revalidateTag&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revalidateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;moveProductToCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newCategoryId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&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;product&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;productId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;categoryId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newCategoryId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`product-detail-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category-listings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`category-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;categoryId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`category-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;newCategoryId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;h2&gt;
  
  
  Implementation Patterns for updateTag
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pattern 1: Simple Field Update
&lt;/h3&gt;

&lt;p&gt;When a user changes a single field, fetch the updated object and pass the relevant fields to &lt;strong&gt;updateTag&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;updateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateUserEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Validate email uniqueness&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;newEmail&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email already in use&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updated&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&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;newEmail&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&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;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pattern 2: Derived Field Update
&lt;/h3&gt;

&lt;p&gt;When the update affects a derived field that appears in the cache (like a denormalized count), fetch the new value and update it:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;updateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addCommentToPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;commentText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&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;comment&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;commentText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;userId&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;span class="c1"&gt;// Fetch the post to get the new comment count&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;postId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;comments&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="p"&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;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`post-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;commentCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;latestComment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;comment&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pattern 3: Conditional Updates Based on Visibility
&lt;/h3&gt;

&lt;p&gt;When an update affects visibility or privacy settings, pass the full updated object if the change is significant:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;updateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;changePostVisibility&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;private&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;friends&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updated&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;postId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;visibility&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`post-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;visibilityChangedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementation Patterns for revalidateTag
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pattern 1: Simple Tag Invalidation
&lt;/h3&gt;

&lt;p&gt;When a resource is deleted or fundamentally changed, invalidate its primary tag:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revalidateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deletePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Verify ownership&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;postId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;postId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`post-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-posts-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pattern 2: Cascading Invalidations
&lt;/h3&gt;

&lt;p&gt;When a change ripples across multiple dependent caches, invalidate all affected tags:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revalidateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deleteUserAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Soft delete to preserve data integrity&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;deletedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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;span class="c1"&gt;// Cascade invalidations&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-profile-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-posts-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-comments-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-followers-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-following-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;homepage-users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-directory&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pattern 3: Batch Operations with Aggregated Invalidation
&lt;/h3&gt;

&lt;p&gt;When an operation affects multiple resources, batch the invalidations:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revalidateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;publishMultiplePosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;postIds&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;published&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;publishedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Invalidate each post individually and aggregate tags&lt;/span&gt;
  &lt;span class="nx"&gt;postIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`post-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Invalidate aggregated views&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;homepage-feed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;latest-posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&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;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;postIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;h2&gt;
  
  
  Measuring Cache Efficiency
&lt;/h2&gt;

&lt;p&gt;To understand whether &lt;strong&gt;updateTag&lt;/strong&gt; is actually delivering efficiency in your application, measure the execution time of the update operation and compare it to a full revalidation. This requires instrumenting your Server Actions with timing code.&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;revalidateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;benchmarkCacheApproach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Approach 1: updateTag&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updateStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;productId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newPrice&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`product-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newPrice&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;updateTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;updateStart&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`updateTag took &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;updateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="s2"&gt;ms`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// For comparison, measure what revalidateTag would do&lt;/span&gt;
  &lt;span class="c1"&gt;// (This is illustrative; don't actually do both in production)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;revalidateStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// A full revalidate would re-execute the product data fetch&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;product&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;productId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;take&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;revalidateTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;revalidateStart&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`revalidateTag equivalent would take &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;revalidateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="s2"&gt;ms`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;updateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;revalidateTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;revalidateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;savings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;revalidateTime&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;updateTime&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;In production applications, &lt;strong&gt;updateTag&lt;/strong&gt; typically executes in 1-5 milliseconds because it only updates the cache entry. &lt;strong&gt;revalidateTag&lt;/strong&gt; forces a full data fetch and re-render, which commonly takes 50-500 milliseconds depending on the complexity of the query and render logic. For high-traffic applications where thousands of cache updates happen per minute, the difference compounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls and How to Avoid Them
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pitfall 1: Using updateTag When Derived Data Changes
&lt;/h3&gt;

&lt;p&gt;A common mistake is attempting to use &lt;strong&gt;updateTag&lt;/strong&gt; when the cached object includes fields that depend on other data in the database. Consider a user's follower count. If we cache &lt;code&gt;{ id, name, followerCount: 150 }&lt;/code&gt; and someone follows the user, we might try to update only the follower count. But if multiple follow operations happen concurrently, race conditions can create incorrect counts.&lt;/p&gt;

&lt;p&gt;Instead, use &lt;strong&gt;revalidateTag&lt;/strong&gt; for fields whose correctness depends on transactional consistency:&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;// Wrong: assumes single-threaded execution&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;followUserWrong&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;followerUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;follows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;followerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;followerUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;followingId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;targetUserId&lt;/span&gt; &lt;span class="p"&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;cachedUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getCachedUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetUserId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;targetUserId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;followerCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cachedUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;followerCount&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;span class="c1"&gt;// Correct: use revalidateTag to re-fetch the count&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;followUserRight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;followerUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;follows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;followerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;followerUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;followingId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;targetUserId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-social-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;targetUserId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;h3&gt;
  
  
  Pitfall 2: Partial Updates That Omit Required Fields
&lt;/h3&gt;

&lt;p&gt;When using &lt;strong&gt;updateTag&lt;/strong&gt;, include all fields that the cached object needs. Partial updates that omit required fields can break downstream code that consumes the cache:&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;// Wrong: missing 'updatedAt' field&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateUserStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Correct: include all essential fields&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateUserStatusCorrect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&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;updated&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&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="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&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;h3&gt;
  
  
  Pitfall 3: Forgetting to Invalidate Related Tags
&lt;/h3&gt;

&lt;p&gt;When using &lt;strong&gt;revalidateTag&lt;/strong&gt;, it is easy to invalidate the primary resource but forget about secondary caches that include that resource. If a user deletes a comment, invalidate both the specific comment tag and the parent article's comment section tag:&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;// Wrong: only invalidates the comment itself&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deleteCommentWrong&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&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;comment&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;commentId&lt;/span&gt; &lt;span class="p"&gt;}&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;commentId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`comment-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;commentId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Correct: invalidates both the comment and its parent context&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deleteCommentCorrect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&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;comment&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;commentId&lt;/span&gt; &lt;span class="p"&gt;}&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;commentId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`comment-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;commentId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`article-comments-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;articleId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;h2&gt;
  
  
  Combining updateTag and revalidateTag in Complex Workflows
&lt;/h2&gt;

&lt;p&gt;Real applications rarely use only one function. A sophisticated caching strategy applies both, each where appropriate. Here is a complete example from an e-commerce platform:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;revalidateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// When stock changes for a single product, update surgically&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decrementStock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&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;product&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;productId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;decrement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;quantity&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;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`product-stock-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lastUpdated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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;span class="c1"&gt;// When a product is created, invalidate aggregate listings&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createProduct&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;CreateProductInput&lt;/span&gt;&lt;span class="p"&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;product&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all-products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`category-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;categoryId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;homepage-featured&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// When a product's category changes, invalidate both old and new category caches&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;changeProductCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newCategoryId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&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;product&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;productId&lt;/span&gt; &lt;span class="p"&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;updated&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;productId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;categoryId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newCategoryId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`category-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;categoryId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`category-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;newCategoryId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// Stock cache is still valid, so don't invalidate it&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// When a review is added, update the product's review summary&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addProductReview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&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;review&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&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;stats&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;_avg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rating&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;_count&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="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Update only the review stats, not the entire product&lt;/span&gt;
  &lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`product-reviews-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;averageRating&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_avg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reviewCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;review&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern demonstrates the practical reality: &lt;strong&gt;updateTag&lt;/strong&gt; handles localized changes that do not affect dependent caches, while &lt;strong&gt;revalidateTag&lt;/strong&gt; ensures correctness when changes propagate across the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Monitoring and Observability
&lt;/h2&gt;

&lt;p&gt;To ensure your cache strategy is actually improving performance, instrument your application to track cache hit rates and invalidation frequency:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;revalidateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateProductWithMetrics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&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;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;updated&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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;productId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`product-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;span class="nx"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&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;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;

  &lt;span class="c1"&gt;// Log metrics for observability&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cache_update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aggregate these logs to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How often each tag is invalidated&lt;/li&gt;
&lt;li&gt;The distribution of update times&lt;/li&gt;
&lt;li&gt;Whether specific operations are slower than expected&lt;/li&gt;
&lt;li&gt;Correlated increases in invalidation frequency with traffic spikes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For data-intensive applications, these metrics reveal whether your &lt;strong&gt;updateTag&lt;/strong&gt; vs. &lt;strong&gt;revalidateTag&lt;/strong&gt; strategy is actually reducing computational burden or whether you would benefit from rebalancing.&lt;/p&gt;

&lt;p&gt;The technical distinction between &lt;strong&gt;updateTag&lt;/strong&gt; and &lt;strong&gt;revalidateTag&lt;/strong&gt; is simple: one updates cache in place, the other purges and rebuilds. The practical distinction is profound. Applying the right function at the right time transforms cache management from a source of staleness or wasted computation into a precise tool that keeps data fresh without drowning under the weight of full recomputes.&lt;/p&gt;

&lt;p&gt;For projects requiring professional Web3 documentation or a full-stack Next.js application, visit &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;https://fiverr.com/meric_cintosun&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>nextjs</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Reducing Network Overhead with Layout Deduplication in the Next.js App Router</title>
      <dc:creator>Meriç Cintosun</dc:creator>
      <pubDate>Wed, 08 Apr 2026 15:21:20 +0000</pubDate>
      <link>https://forem.com/mericcintosun/reducing-network-overhead-with-layout-deduplication-in-the-nextjs-app-router-m7b</link>
      <guid>https://forem.com/mericcintosun/reducing-network-overhead-with-layout-deduplication-in-the-nextjs-app-router-m7b</guid>
      <description>&lt;h2&gt;
  
  
  Why Layout Deduplication Exists
&lt;/h2&gt;

&lt;p&gt;Every meaningful performance optimization in a framework emerges from a concrete bottleneck that enough production applications hit. Layout deduplication in the Next.js App Router addresses a specific class of redundancy that becomes visible at scale: when a user navigates between dozens of routes that share a common layout tree, the browser should not pay the cost of re-fetching or re-evaluating static portions of that tree on each navigation. In an e-commerce application with 50 product category pages all mounted under the same shell, the problem is not theoretical.&lt;/p&gt;

&lt;p&gt;The App Router's file-system routing model introduced nested layouts as a first-class primitive. A &lt;code&gt;layout.tsx&lt;/code&gt; file at any directory level wraps all routes beneath it, and that wrapper persists across navigations within its subtree. The deduplication work extends this mental model into the prefetching layer: when the router prefetches multiple links simultaneously, it should recognize that many of those targets share layout segments and issue only the work necessary for their diverging leaves.&lt;/p&gt;

&lt;p&gt;Understanding the engineering decisions behind this requires understanding how the router represents routes, how prefetch payloads are structured, and where the React Server Components protocol intersects with HTTP cache semantics. We will cover each of these before examining how the optimization manifests in a real e-commerce scenario.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the App Router Represents Routes Internally
&lt;/h2&gt;

&lt;p&gt;The App Router models every URL as a tree of segments. A URL like &lt;code&gt;/shop/electronics/laptops&lt;/code&gt; decomposes into the segments &lt;code&gt;shop&lt;/code&gt;, &lt;code&gt;electronics&lt;/code&gt;, and &lt;code&gt;laptops&lt;/code&gt;, each of which may have an associated &lt;code&gt;layout.tsx&lt;/code&gt;, &lt;code&gt;page.tsx&lt;/code&gt;, &lt;code&gt;loading.tsx&lt;/code&gt;, and &lt;code&gt;error.tsx&lt;/code&gt;. The router stores its understanding of the current UI as a segment tree in a client-side cache.&lt;/p&gt;

&lt;p&gt;Each node in that cache holds a React Server Component (RSC) payload for its segment. RSC payloads are serialized representations of server-rendered component trees, transmitted as a streaming text format over the network. When a navigation occurs, the router compares the incoming segment tree against the cached one and requests only the RSC payloads for segments that have changed. Segments whose layout files have not changed and whose props remain identical are served directly from the client cache.&lt;/p&gt;

&lt;p&gt;This diffing is possible because the App Router assigns a stable identity to each layout segment based on its file path. &lt;code&gt;/app/shop/layout.tsx&lt;/code&gt; always maps to the &lt;code&gt;shop&lt;/code&gt; layout node regardless of what leaf page is active beneath it. The router can therefore determine before issuing any network request whether a given layout segment is already in cache and still valid.&lt;/p&gt;

&lt;h3&gt;
  
  
  The RSC Payload Format and Segment Boundaries
&lt;/h3&gt;

&lt;p&gt;RSC payloads use a line-delimited JSON format (sometimes called the Flight protocol). Each line encodes a chunk of the virtual DOM tree, and chunks reference each other by integer identifiers. A simplified payload for a page under a shared layout looks approximately like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"div"&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="nl"&gt;"className"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"shop-shell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"children"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"$L2"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"main"&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="nl"&gt;"children"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"$L3"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"article"&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="nl"&gt;"children"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Laptop Pro 14 details..."&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The layout contributes chunks 1 and 2. The page contributes chunk 3. When the router already holds chunks 1 and 2 in its segment cache, it can request a payload that starts at chunk 3, skipping the layout entirely. The server must therefore be capable of rendering and streaming partial subtrees, starting from any segment boundary. Next.js accomplishes this through its internal &lt;code&gt;renderToReadableStream&lt;/code&gt; pipeline, which accepts a &lt;code&gt;renderOpts.postponed&lt;/code&gt; mechanism that allows resuming rendering from a specific point in the tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefetching: The Problem Space
&lt;/h2&gt;

&lt;p&gt;Navigation in a modern web application is not purely reactive. When a user's cursor moves toward a link or when a link enters the viewport, the framework can speculatively fetch the destination page before the click occurs. Next.js implements this through the &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; component, which triggers a prefetch when a link becomes visible in the viewport by default.&lt;/p&gt;

&lt;p&gt;The problem emerges when a page renders many links simultaneously. Consider a category listing page in an e-commerce application: it may render 50 product cards, each linking to a product detail page. All 50 product pages share the same layout tree: &lt;code&gt;RootLayout -&amp;gt; ShopLayout -&amp;gt; ProductLayout -&amp;gt; ProductPage&lt;/code&gt;. If the browser fires a prefetch request for each link as it enters the viewport, the server receives 50 requests. Without deduplication, the response to each request includes the RSC payload for &lt;code&gt;RootLayout&lt;/code&gt;, &lt;code&gt;ShopLayout&lt;/code&gt;, and &lt;code&gt;ProductLayout&lt;/code&gt;, plus the leaf &lt;code&gt;ProductPage&lt;/code&gt; payload. The shared layout data is transmitted 50 times.&lt;/p&gt;

&lt;p&gt;At a conservative estimate, a typical React Server Component layout for a shop shell might serialize to 8 to 15 kilobytes of RSC payload. Multiplied across 50 prefetch requests, that means 400 to 750 kilobytes of redundant layout data on top of the actual page content. On a mobile connection or in a region with high latency, this consumes bandwidth that should be reserved for content the user will actually read.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prefetch Granularity Before Deduplication
&lt;/h3&gt;

&lt;p&gt;Before layout deduplication, the prefetch system operated at the route level. A prefetch for &lt;code&gt;/shop/products/laptop-pro-14&lt;/code&gt; would return the full RSC payload for the entire route, from root layout to leaf page. The client cache stored the result keyed by the full pathname. Two prefetch requests for &lt;code&gt;/shop/products/laptop-pro-14&lt;/code&gt; and &lt;code&gt;/shop/products/gaming-chair-x&lt;/code&gt; would both return full payloads including the identical &lt;code&gt;ShopLayout&lt;/code&gt; and &lt;code&gt;ProductLayout&lt;/code&gt; segments.&lt;/p&gt;

&lt;p&gt;The client cache did store these payloads and would serve subsequent navigations from cache, but the network cost during the prefetch phase was fully duplicated. The deduplication work moves the optimization earlier, into the prefetch request itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layout Deduplication: The Engineering Approach
&lt;/h2&gt;

&lt;p&gt;Layout deduplication restructures prefetching around segment-level cache keys rather than full-pathname keys. The router maintains a segment cache that maps a &lt;code&gt;(layoutPath, params)&lt;/code&gt; tuple to an RSC payload promise. When a prefetch request is about to be issued, the router first checks which segments along the target route are already in the segment cache. It then requests only the missing segments from the server.&lt;/p&gt;

&lt;p&gt;The server-side rendering pipeline exposes a &lt;code&gt;prefetchSegments&lt;/code&gt; API that accepts a list of segment paths and returns a response containing only those segments' RSC payloads. Two concurrent prefetch requests for two different product pages will both find &lt;code&gt;ShopLayout&lt;/code&gt; already in-flight or cached after the first request resolves, and neither will request it again.&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;// Simplified representation of the segment cache lookup&lt;/span&gt;
&lt;span class="c1"&gt;// in the Next.js client router internals&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SegmentCacheEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RSCPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;SegmentCacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// layoutPath:serializedParams&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;segmentCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SegmentCacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SegmentCacheEntry&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCacheKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;layoutPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;SegmentCacheKey&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;layoutPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;prefetchRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;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;segments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseRouteIntoSegments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;missingSegments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RouteSegment&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="k"&gt;for &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;segment&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getCacheKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;layoutPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;segmentCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;isCacheStale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;segmentCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&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="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;missingSegments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;segment&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;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;missingSegments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetchSegmentPayloads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;missingSegments&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &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;segment&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;missingSegments&lt;/span&gt;&lt;span class="p"&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getCacheKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;layoutPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;segmentCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;PREFETCH_CACHE_TTL&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual Next.js implementation is more involved, handling streaming, suspense boundaries, and cache invalidation on mutation, but the core logic follows this shape. The segment cache entries store promises, which means concurrent prefetch requests for the same segment key will await the same in-flight network request rather than issuing duplicates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deduplication of In-Flight Requests
&lt;/h3&gt;

&lt;p&gt;Promise-based cache entries handle the race condition where multiple links enter the viewport near-simultaneously and all trigger prefetches before any response has arrived. When the first prefetch for &lt;code&gt;ShopLayout&lt;/code&gt; begins, the cache stores a pending promise at that key. Every subsequent prefetch that needs &lt;code&gt;ShopLayout&lt;/code&gt; finds the pending promise and awaits it rather than starting a new request. The network connection is established exactly once per unique segment.&lt;/p&gt;

&lt;p&gt;This behavior is analogous to how &lt;code&gt;swr&lt;/code&gt; and &lt;code&gt;react-query&lt;/code&gt; implement request deduplication for data fetching. The key insight is that the cache must store the promise itself, not the resolved value, so that in-flight requests can be shared.&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;// Demonstrating promise deduplication for concurrent prefetches&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchSegmentPayloads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RouteSegment&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SegmentCacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RSCPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildPrefetchURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// This fetch is shared across all callers awaiting the same segments&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&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;RSC&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;1&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;Next-Router-Prefetch&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;1&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;parseMultiSegmentResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&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;The &lt;code&gt;Next-Router-Prefetch&lt;/code&gt; header signals to the server that this is a prefetch request. The server uses this signal to adjust rendering behavior: it renders only the static shell of server components and defers dynamic content that would be streamed on actual navigation. This distinction matters because a prefetch should not waste server CPU on personalized or frequently changing data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying This to an E-Commerce Application
&lt;/h2&gt;

&lt;p&gt;Consider a product listing page that renders 50 product cards. Each card links to a product detail page. The routing structure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/app
  layout.tsx          &amp;lt;- RootLayout (nav, footer, global providers)
  /shop
    layout.tsx        &amp;lt;- ShopLayout (sidebar, category nav)
    /products
      layout.tsx      &amp;lt;- ProductLayout (breadcrumbs, structured data wrapper)
      /[slug]
        page.tsx      &amp;lt;- ProductPage (images, description, price, CTA)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without layout deduplication, prefetching all 50 links issues 50 requests, each returning payloads for &lt;code&gt;RootLayout&lt;/code&gt;, &lt;code&gt;ShopLayout&lt;/code&gt;, &lt;code&gt;ProductLayout&lt;/code&gt;, and &lt;code&gt;ProductPage&lt;/code&gt;. With deduplication, the first prefetch request fetches all four segments for the first product. The 49 subsequent requests find &lt;code&gt;RootLayout&lt;/code&gt;, &lt;code&gt;ShopLayout&lt;/code&gt;, and &lt;code&gt;ProductLayout&lt;/code&gt; already cached and request only the &lt;code&gt;ProductPage&lt;/code&gt; segment for each remaining product.&lt;/p&gt;

&lt;p&gt;The network savings scale with the depth of the shared layout tree and the number of co-located links. Three shared layout segments at an average of 10 kilobytes each is 30 kilobytes per prefetch. Across 49 additional navigations, deduplication saves approximately 1.47 megabytes of prefetch traffic. The actual savings depend on how much JavaScript and component state each layout serializes into its RSC payload.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring Prefetch Behavior for Maximum Benefit
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; component's &lt;code&gt;prefetch&lt;/code&gt; prop controls when prefetching occurs. The default behavior in the App Router prefetches the static layout shell immediately when a link enters the viewport and defers the dynamic leaf page until hover. This two-phase approach fits well with the deduplication model: shared layout segments are fetched and cached early, and only the cheap leaf segments are fetched on hover.&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="c1"&gt;// app/shop/products/page.tsx&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Link&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getProducts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/products&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductListingPage&lt;/span&gt;&lt;span class="p"&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;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getProducts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"product-grid"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/*
            Default prefetch behavior: prefetches static layout shell
            on viewport entry, full page on hover.
            Set prefetch={false} to disable entirely for low-priority links.
          */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`/shop/products/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thumbnailUrl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;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;For server components that render large lists, it is worth measuring the prefetch volume before and after. The Next.js DevTools panel in the browser shows prefetch requests grouped by segment. A healthy prefetch pattern for 50 links sharing three layout levels should show three segment requests for the first navigation target and one segment request for each subsequent target once the layout cache is warm.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache Invalidation and Stale Layouts
&lt;/h3&gt;

&lt;p&gt;The segment cache respects the &lt;code&gt;staleTime&lt;/code&gt; that Next.js assigns to prefetch entries, which defaults to 30 seconds for static segments and 0 seconds for dynamic segments. A &lt;code&gt;ShopLayout&lt;/code&gt; that reads from a database on every request will be marked dynamic and will not benefit from cross-route deduplication, because each prefetch must re-fetch it to get fresh data.&lt;/p&gt;

&lt;p&gt;The practical advice here is to push dynamic data as far down the component tree as possible. If the sidebar in &lt;code&gt;ShopLayout&lt;/code&gt; shows a cart item count, consider moving that count into a client component that fetches independently from the layout's RSC payload. The layout itself becomes static and cacheable, while the cart count fetches on the client after hydration.&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="c1"&gt;// app/shop/layout.tsx&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Suspense&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CategoryNav&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/CategoryNav&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CartCountBadge&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/CartCountBadge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// client component&lt;/span&gt;

&lt;span class="c1"&gt;// This layout is now static: no await calls that produce dynamic output.&lt;/span&gt;
&lt;span class="c1"&gt;// Next.js will mark it as static and the segment cache will hold it&lt;/span&gt;
&lt;span class="c1"&gt;// for the full staleTime window.&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;ShopLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"shop-wrapper"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;aside&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CategoryNav&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* CartCountBadge fetches client-side; does not make this layout dynamic */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CartCountBadge&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;aside&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;h2&gt;
  
  
  Measuring the Impact
&lt;/h2&gt;

&lt;p&gt;Quantifying the savings from layout deduplication requires measuring at the network layer, not just in Lighthouse scores. Lighthouse runs a single navigation and does not simulate concurrent prefetches. The more useful measurement is the total bytes transferred during a browsing session that includes entering a listing page and hovering over several product links.&lt;/p&gt;

&lt;p&gt;The browser's Network panel in DevTools, filtered by &lt;code&gt;RSC&lt;/code&gt; requests (look for requests with the &lt;code&gt;RSC: 1&lt;/code&gt; header), shows the payload sizes for each prefetch. Comparing the total RSC bytes with deduplication enabled against a baseline with &lt;code&gt;prefetch={false}&lt;/code&gt; across all links gives a concrete transfer savings number.&lt;/p&gt;

&lt;p&gt;A server-side perspective requires logging the segments requested in each prefetch call. Next.js exposes the requested segment paths in the URL of the prefetch request, typically as query parameters like &lt;code&gt;?_rsc=segmentHash&lt;/code&gt;. Aggregating these logs over a session shows how often shared segments are requested versus served from client cache.&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="c"&gt;# Example: using curl to simulate a prefetch request and inspect response size&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{size_download}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"RSC: 1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Next-Router-Prefetch: 1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://your-app.com/shop/products/laptop-pro-14"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this command against a route and then comparing it to a request that omits the shared layout segments demonstrates the per-request savings. In a well-structured application, the layout-only prefetch payload for three layout levels should be significantly smaller than the full-route payload, often by 40 to 60 percent of the total prefetch transfer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interaction with the HTTP Cache
&lt;/h2&gt;

&lt;p&gt;The App Router's segment-level prefetch requests are standard HTTP requests and participate in the HTTP cache. When a CDN sits in front of the Next.js application, layout segment responses can be cached at the edge with &lt;code&gt;Cache-Control: public, max-age=30, stale-while-revalidate=60&lt;/code&gt; headers. Subsequent users who visit the same listing page will receive layout segment prefetches from the CDN rather than from the origin.&lt;/p&gt;

&lt;p&gt;This layering of client segment cache and HTTP edge cache produces a hierarchy of deduplication. At the client level, a single user browsing 50 product links pays the layout segment cost once per session. At the CDN level, many users browsing the same listing page all receive layout prefetches from cache. The origin server handles only the dynamic leaf page segments that cannot be cached.&lt;/p&gt;

&lt;p&gt;Next.js automatically sets &lt;code&gt;Cache-Control&lt;/code&gt; headers based on whether a segment is static or dynamic. Static segments receive a &lt;code&gt;s-maxage&lt;/code&gt; value that allows CDN caching. Dynamic segments receive &lt;code&gt;no-store&lt;/code&gt;. Ensuring that shared layout segments remain static therefore produces benefits at both the client deduplication layer and the HTTP cache layer simultaneously.&lt;/p&gt;

&lt;p&gt;If you work on professional Web3 documentation or need a production-grade Next.js application built end to end, take a look at the services available at &lt;a href="https://fiverr.com/meric_cintosun" rel="noopener noreferrer"&gt;fiverr.com/meric_cintosun&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>performance</category>
      <category>react</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
