<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://rhymion.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://rhymion.github.io/" rel="alternate" type="text/html" /><updated>2026-03-16T02:18:13+00:00</updated><id>https://rhymion.github.io/feed.xml</id><title type="html">Rhymion Labs</title><subtitle>Building generative structures that maximize potential.</subtitle><entry><title type="html">Dark Mode And Hydration In Nextjs With Mui</title><link href="https://rhymion.github.io/blog/2026/03/16/dark-mode-and-hydration-in-nextjs-with-mui/" rel="alternate" type="text/html" title="Dark Mode And Hydration In Nextjs With Mui" /><published>2026-03-16T00:00:00+00:00</published><updated>2026-03-16T00:00:00+00:00</updated><id>https://rhymion.github.io/blog/2026/03/16/dark-mode-and-hydration-in-nextjs-with-mui</id><content type="html" xml:base="https://rhymion.github.io/blog/2026/03/16/dark-mode-and-hydration-in-nextjs-with-mui/"><![CDATA[<h1 id="dark-mode-and-hydration-errors-in-nextjs-app-router-with-mui">Dark Mode and Hydration Errors in Next.js App Router with MUI</h1>

<p>Adding dark mode to a <a href="https://nextjs.org/">Next.js</a> App Router application sounds
straightforward — enable a theme option and you’re done. In practice, combining
<a href="https://mui.com/">Material UI (MUI)</a> with Next.js App Router introduces a subtle
set of pitfalls around <strong>React hydration errors</strong> that can be hard to diagnose
and harder to fix if you encounter them in the wrong order.</p>

<p>This article walks through the problem, why it happens, and the correct setup
sequence to get dark mode working without hydration errors.</p>

<hr />

<h2 id="background-what-is-hydration">Background: What Is Hydration?</h2>

<p>Next.js renders pages on the server and sends the resulting HTML to the browser.
React then runs on the client and “hydrates” that HTML — it attaches event
handlers and takes over control of the DOM. For this to work, the HTML structure
React produces on the client must exactly match what the server sent.</p>

<p>When there’s a mismatch, React throws a hydration error:</p>

<blockquote>
  <p>“Hydration failed because the server rendered HTML didn’t match the client.”</p>
</blockquote>

<p>These errors are often invisible during development but appear in production, or
they can cause a blank page or a broken layout. See the
<a href="https://react.dev/reference/react-dom/client/hydrateRoot#troubleshooting">React docs on hydration</a>
for a deeper explanation.</p>

<hr />

<h2 id="background-how-mui-styles-work">Background: How MUI Styles Work</h2>

<p>MUI uses <a href="https://emotion.sh/docs/introduction">Emotion</a> as its CSS-in-JS engine.
Emotion generates <code class="language-plaintext highlighter-rouge">&lt;style&gt;</code> tags at runtime. In a server-rendered app, Emotion also
generates those styles on the server and embeds them in the initial HTML so the page
doesn’t flash unstyled content.</p>

<p>The order and position of those <code class="language-plaintext highlighter-rouge">&lt;style&gt;</code> tags must be identical between the server
and the client. If they differ, React’s hydration check fails.</p>

<hr />

<h2 id="the-trap-adding-themeprovider-before-approutercacheprovider">The Trap: Adding ThemeProvider Before AppRouterCacheProvider</h2>

<p>A common mistake is to install MUI and immediately add a <code class="language-plaintext highlighter-rouge">ThemeProvider</code>:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app/layout.tsx — DO NOT do this yet</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">ThemeProvider</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@mui/material/styles</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">CssBaseline</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@mui/material/CssBaseline</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">RootLayout</span><span class="p">({</span> <span class="nx">children</span> <span class="p">})</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">(</span>
    <span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nc">ThemeProvider</span> <span class="na">theme</span><span class="p">=</span><span class="si">{</span><span class="nx">theme</span><span class="si">}</span><span class="p">&gt;</span>
          <span class="p">&lt;</span><span class="nc">CssBaseline</span> <span class="p">/&gt;</span>
          <span class="si">{</span><span class="nx">children</span><span class="si">}</span>
        <span class="p">&lt;/</span><span class="nc">ThemeProvider</span><span class="p">&gt;</span>
      <span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Without the correct Emotion cache setup, adding <code class="language-plaintext highlighter-rouge">ThemeProvider</code> triggers a cascade
of SSR style injection that produces hydration errors. You may see:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Hydration failed because the server rendered HTML didn't match the client</code></li>
  <li><code class="language-plaintext highlighter-rouge">Cannot read properties of null (reading 'parentNode')</code></li>
</ul>

<p>What makes this tricky is that <em>without</em> <code class="language-plaintext highlighter-rouge">ThemeProvider</code>, Emotion generates very
little CSS during SSR, so the bug stays hidden. As soon as <code class="language-plaintext highlighter-rouge">ThemeProvider</code> is
added, Emotion generates substantial CSS server-side and injects <code class="language-plaintext highlighter-rouge">&lt;style&gt;</code> tags in
a position that conflicts with React’s hydration.</p>

<hr />

<h2 id="step-1-install-and-configure-approutercacheprovider">Step 1: Install and Configure AppRouterCacheProvider</h2>

<p><a href="https://mui.com/material-ui/integrations/nextjs/"><code class="language-plaintext highlighter-rouge">AppRouterCacheProvider</code></a> is
MUI’s official adapter for the Next.js App Router. It ensures Emotion’s style cache
is shared correctly across SSR and client hydration, so <code class="language-plaintext highlighter-rouge">&lt;style&gt;</code> tags are inserted
in the same order and position on both sides.</p>

<p>Install the package:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> @mui/material-nextjs
</code></pre></div></div>

<p>Then wrap the root layout’s body content:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app/layout.tsx</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AppRouterCacheProvider</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@mui/material-nextjs/v15-appRouter</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">RootLayout</span><span class="p">({</span> <span class="nx">children</span> <span class="p">})</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">(</span>
    <span class="p">&lt;</span><span class="nt">html</span> <span class="na">lang</span><span class="p">=</span><span class="s">"en"</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nc">AppRouterCacheProvider</span><span class="p">&gt;</span>
          <span class="si">{</span><span class="nx">children</span><span class="si">}</span>
        <span class="p">&lt;/</span><span class="nc">AppRouterCacheProvider</span><span class="p">&gt;</span>
      <span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This must be in place <strong>before</strong> you add <code class="language-plaintext highlighter-rouge">ThemeProvider</code>. If you add <code class="language-plaintext highlighter-rouge">ThemeProvider</code>
first, you’ll encounter hydration errors that are difficult to trace back to the
missing cache provider.</p>

<hr />

<h2 id="step-2-add-themeprovider-with-dark-mode">Step 2: Add ThemeProvider with Dark Mode</h2>

<p>Once <code class="language-plaintext highlighter-rouge">AppRouterCacheProvider</code> is in place, you can safely add a <code class="language-plaintext highlighter-rouge">ThemeProvider</code>.
Because <code class="language-plaintext highlighter-rouge">ThemeProvider</code> and <code class="language-plaintext highlighter-rouge">CssBaseline</code> are client components (they use React
context), it’s cleanest to extract them into a dedicated <code class="language-plaintext highlighter-rouge">Providers</code> component:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app/providers.tsx</span>
<span class="dl">'</span><span class="s1">use client</span><span class="dl">'</span><span class="p">;</span>

<span class="k">import</span> <span class="p">{</span> <span class="nx">createTheme</span><span class="p">,</span> <span class="nx">ThemeProvider</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@mui/material/styles</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">CssBaseline</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@mui/material/CssBaseline</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">theme</span> <span class="o">=</span> <span class="nx">createTheme</span><span class="p">({</span>
  <span class="na">colorSchemes</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">dark</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="na">cssVariables</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">colorSchemeSelector</span><span class="p">:</span> <span class="dl">'</span><span class="s1">media</span><span class="dl">'</span><span class="p">,</span>
  <span class="p">},</span>
<span class="p">});</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">Providers</span><span class="p">({</span> <span class="nx">children</span> <span class="p">}:</span> <span class="p">{</span> <span class="nl">children</span><span class="p">:</span> <span class="nx">React</span><span class="p">.</span><span class="nx">ReactNode</span> <span class="p">})</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">(</span>
    <span class="p">&lt;</span><span class="nc">ThemeProvider</span> <span class="na">theme</span><span class="p">=</span><span class="si">{</span><span class="nx">theme</span><span class="si">}</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nc">CssBaseline</span> <span class="na">enableColorScheme</span> <span class="p">/&gt;</span>
      <span class="si">{</span><span class="nx">children</span><span class="si">}</span>
    <span class="p">&lt;/</span><span class="nc">ThemeProvider</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then use it in the layout:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app/layout.tsx</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AppRouterCacheProvider</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@mui/material-nextjs/v15-appRouter</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">Providers</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./providers</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">RootLayout</span><span class="p">({</span> <span class="nx">children</span> <span class="p">})</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">(</span>
    <span class="p">&lt;</span><span class="nt">html</span> <span class="na">lang</span><span class="p">=</span><span class="s">"en"</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nc">AppRouterCacheProvider</span><span class="p">&gt;</span>
          <span class="p">&lt;</span><span class="nc">Providers</span><span class="p">&gt;</span>
            <span class="si">{</span><span class="nx">children</span><span class="si">}</span>
          <span class="p">&lt;/</span><span class="nc">Providers</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nc">AppRouterCacheProvider</span><span class="p">&gt;</span>
      <span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="why-colorschemeselector-media">Why <code class="language-plaintext highlighter-rouge">colorSchemeSelector: 'media'</code></h2>

<p>MUI v6+ supports two approaches for activating dark mode:</p>

<table>
  <thead>
    <tr>
      <th>Approach</th>
      <th>How it works</th>
      <th>SSR-safe?</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">'class'</code></td>
      <td>Adds a <code class="language-plaintext highlighter-rouge">.dark</code> class to <code class="language-plaintext highlighter-rouge">&lt;html&gt;</code> via a JS script</td>
      <td>Requires <code class="language-plaintext highlighter-rouge">getInitColorSchemeScript</code>, which is client-only</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">'media'</code></td>
      <td>Scopes CSS variables inside <code class="language-plaintext highlighter-rouge">@media (prefers-color-scheme: dark)</code></td>
      <td>Yes — pure CSS, no script needed</td>
    </tr>
  </tbody>
</table>

<p>The <code class="language-plaintext highlighter-rouge">'class'</code> approach requires injecting
<a href="https://mui.com/material-ui/customization/css-theme-variables/usage/#server-side-rendering"><code class="language-plaintext highlighter-rouge">getInitColorSchemeScript</code></a>
into the page before React hydrates. However, this API is client-only and cannot
be called from a Next.js Server Component — attempting it throws a runtime error.</p>

<p>The <code class="language-plaintext highlighter-rouge">'media'</code> approach uses a CSS media query to apply dark-mode CSS variables.
Since the CSS is identical on server and client, there is no hydration mismatch.
This is the right default for Next.js App Router.</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">/* What MUI generates under the hood with colorSchemeSelector: 'media' */</span>
<span class="k">@media</span> <span class="p">(</span><span class="n">prefers-color-scheme</span><span class="p">:</span> <span class="n">dark</span><span class="p">)</span> <span class="p">{</span>
  <span class="nd">:root</span> <span class="p">{</span>
    <span class="py">--mui-palette-background-default</span><span class="p">:</span> <span class="m">#121212</span><span class="p">;</span>
    <span class="py">--mui-palette-text-primary</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span>
    <span class="c">/* ... */</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Dark mode activates automatically based on the OS/browser preference — no
JavaScript is required at runtime.</p>

<hr />

<h2 id="why-cssbaseline-enablecolorscheme">Why <code class="language-plaintext highlighter-rouge">CssBaseline enableColorScheme</code></h2>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nc">CssBaseline</span> <span class="na">enableColorScheme</span> <span class="p">/&gt;</span>
</code></pre></div></div>

<p><a href="https://mui.com/material-ui/react-css-baseline/"><code class="language-plaintext highlighter-rouge">CssBaseline</code></a> resets browser
default styles (similar to <a href="https://necolas.github.io/normalize.css/">normalize.css</a>).
The <code class="language-plaintext highlighter-rouge">enableColorScheme</code> prop adds <code class="language-plaintext highlighter-rouge">color-scheme: light dark</code> to <code class="language-plaintext highlighter-rouge">&lt;body&gt;</code>, which
tells the browser to adapt its native controls — scrollbars, form inputs, select
dropdowns, date pickers — to the active color scheme. Without it, browser-native
UI elements remain light even when MUI components are dark.</p>

<hr />

<h2 id="hardcoded-colors-the-hidden-dark-mode-bug">Hardcoded Colors: The Hidden Dark Mode Bug</h2>

<p>MUI components adapt automatically once <code class="language-plaintext highlighter-rouge">ThemeProvider</code> is set up. The risk area
is <strong>hardcoded color values</strong> in custom components, which bypass the theme entirely.</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Broken in dark mode — hardcoded light grey</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">style</span><span class="p">=&gt;</span>
  Some content
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">#f5f5f5</code> is near-white. On a dark background it creates a harsh light bar that
looks broken.</p>

<p><strong>Fix:</strong> use MUI’s CSS variables, which are updated automatically by the theme:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Theme-aware — adapts to light and dark mode</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">style</span><span class="p">=&gt;</span>
  Some content
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</code></pre></div></div>

<p>Common MUI CSS variables that adapt to both light and dark mode:</p>

<table>
  <thead>
    <tr>
      <th>Variable</th>
      <th>Use case</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">var(--mui-palette-action-hover)</code></td>
      <td>Subtle tinted background (e.g. info bars)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">var(--mui-palette-background-paper)</code></td>
      <td>Card / paper surface</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">var(--mui-palette-background-default)</code></td>
      <td>Page background</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">var(--mui-palette-text-primary)</code></td>
      <td>Primary text</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">var(--mui-palette-text-secondary)</code></td>
      <td>Muted / secondary text</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">var(--mui-palette-divider)</code></td>
      <td>Borders and dividers</td>
    </tr>
  </tbody>
</table>

<p>Alternatively, use MUI’s
<a href="https://mui.com/system/getting-started/the-sx-prop/"><code class="language-plaintext highlighter-rouge">sx</code> prop</a> or the
<a href="https://mui.com/material-ui/customization/theming/#accessing-the-theme-in-a-component"><code class="language-plaintext highlighter-rouge">useTheme()</code> hook</a>,
which are always theme-aware:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">useTheme</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@mui/material/styles</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">function</span> <span class="nx">MyComponent</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">theme</span> <span class="o">=</span> <span class="nx">useTheme</span><span class="p">();</span>
  <span class="k">return</span> <span class="p">(</span>
    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">style</span><span class="p">=&gt;</span>
      Some content
    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="other-common-hydration-errors">Other Common Hydration Errors</h2>

<h3 id="locale-sensitive-date-formatting">Locale-sensitive date formatting</h3>

<p><code class="language-plaintext highlighter-rouge">Date.toLocaleString()</code> and related methods produce different output depending on
the locale configured in the runtime. The Node.js server may use a system locale
that differs from the user’s browser locale, causing a mismatch.</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Potentially different output on server vs client</span>
<span class="p">&lt;</span><span class="nt">span</span><span class="p">&gt;</span><span class="si">{</span><span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">post</span><span class="p">.</span><span class="nx">created_at</span><span class="p">).</span><span class="nx">toLocaleString</span><span class="p">()</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
</code></pre></div></div>

<p>Two options:</p>

<p><strong>Option A — Suppress the warning</strong> (when the client value is the correct one):</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nt">span</span> <span class="na">suppressHydrationWarning</span><span class="p">&gt;</span>
  <span class="si">{</span><span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">post</span><span class="p">.</span><span class="nx">created_at</span><span class="p">).</span><span class="nx">toLocaleString</span><span class="p">()</span><span class="si">}</span>
<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
</code></pre></div></div>

<p><a href="https://react.dev/reference/react-dom/components/common#common-props"><code class="language-plaintext highlighter-rouge">suppressHydrationWarning</code></a>
tells React to accept the mismatch on that specific element and use the
client-rendered value. Only use it where the mismatch is cosmetic and the client
value is the correct one to show (e.g. the user’s local time format).</p>

<p><strong>Option B — Format with a fixed locale</strong> (consistent on both sides):</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nt">span</span><span class="p">&gt;</span>
  <span class="si">{</span><span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">post</span><span class="p">.</span><span class="nx">created_at</span><span class="p">).</span><span class="nx">toLocaleString</span><span class="p">(</span><span class="dl">'</span><span class="s1">en-US</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">timeZone</span><span class="p">:</span> <span class="dl">'</span><span class="s1">UTC</span><span class="dl">'</span> <span class="p">})</span><span class="si">}</span>
<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
</code></pre></div></div>

<h3 id="browser-only-globals">Browser-only globals</h3>

<p>Accessing <code class="language-plaintext highlighter-rouge">window</code>, <code class="language-plaintext highlighter-rouge">navigator</code>, <code class="language-plaintext highlighter-rouge">localStorage</code>, or <code class="language-plaintext highlighter-rouge">document</code> during render
will throw on the server (where they don’t exist) or produce a mismatch:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Crashes on server</span>
<span class="kd">const</span> <span class="nx">isDark</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">matchMedia</span><span class="p">(</span><span class="dl">'</span><span class="s1">(prefers-color-scheme: dark)</span><span class="dl">'</span><span class="p">).</span><span class="nx">matches</span><span class="p">;</span>
</code></pre></div></div>

<p>Move these reads inside a <code class="language-plaintext highlighter-rouge">useEffect</code> (which only runs on the client) or use a
library like <a href="https://usehooks-ts.com/react-hook/use-media-query"><code class="language-plaintext highlighter-rouge">usehooks-ts</code></a>
that handles this safely.</p>

<h3 id="mathrandom-and-datenow-in-rendered-output">Math.random() and Date.now() in rendered output</h3>

<p>These produce different values on each call, so server and client will never match:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Never do this in rendered output</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="p">=</span><span class="si">{</span><span class="s2">`item-</span><span class="p">${</span><span class="nb">Math</span><span class="p">.</span><span class="nx">random</span><span class="p">()}</span><span class="s2">`</span><span class="si">}</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</code></pre></div></div>

<p>Use a stable ID source instead, such as a database ID or the
<a href="https://react.dev/reference/react/useId"><code class="language-plaintext highlighter-rouge">useId()</code></a> hook.</p>

<hr />

<h2 id="setup-summary-correct-order">Setup Summary (correct order)</h2>

<p>Getting the order right is important — adding <code class="language-plaintext highlighter-rouge">ThemeProvider</code> before
<code class="language-plaintext highlighter-rouge">AppRouterCacheProvider</code> triggers errors that are hard to trace.</p>

<ol>
  <li>Install <code class="language-plaintext highlighter-rouge">@mui/material-nextjs</code></li>
  <li>Add <code class="language-plaintext highlighter-rouge">AppRouterCacheProvider</code> to <code class="language-plaintext highlighter-rouge">app/layout.tsx</code> (wraps the body contents)</li>
  <li>Create a client <code class="language-plaintext highlighter-rouge">Providers</code> component with <code class="language-plaintext highlighter-rouge">ThemeProvider</code> + <code class="language-plaintext highlighter-rouge">CssBaseline</code></li>
  <li>Use <code class="language-plaintext highlighter-rouge">colorSchemeSelector: 'media'</code> in the theme — no init script, SSR-safe</li>
  <li>Replace any hardcoded color values in custom components with MUI CSS variables</li>
</ol>

<hr />

<h2 id="references">References</h2>

<ul>
  <li><a href="https://mui.com/material-ui/integrations/nextjs/">MUI: Next.js App Router integration</a></li>
  <li><a href="https://mui.com/material-ui/customization/dark-mode/">MUI: Dark mode</a></li>
  <li><a href="https://mui.com/material-ui/customization/css-theme-variables/usage/#server-side-rendering">MUI: CSS theme variables — server-side rendering</a></li>
  <li><a href="https://mui.com/material-ui/react-css-baseline/">MUI: CssBaseline</a></li>
  <li><a href="https://mui.com/system/getting-started/the-sx-prop/">MUI: The sx prop</a></li>
  <li><a href="https://nextjs.org/docs/app/building-your-application/rendering">Next.js: App Router — Rendering</a></li>
  <li><a href="https://react.dev/reference/react-dom/client/hydrateRoot#troubleshooting">React: hydrateRoot — troubleshooting</a></li>
  <li><a href="https://react.dev/reference/react-dom/components/common#common-props">React: suppressHydrationWarning</a></li>
  <li><a href="https://react.dev/reference/react/useId">React: useId</a></li>
  <li><a href="https://emotion.sh/docs/ssr">Emotion: Server-side rendering</a></li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[Dark Mode and Hydration Errors in Next.js App Router with MUI]]></summary></entry><entry><title type="html">Improving Next.js App Router Performance with a Remote Database</title><link href="https://rhymion.github.io/blog/2026/03/16/nextjs-performance-remote-database/" rel="alternate" type="text/html" title="Improving Next.js App Router Performance with a Remote Database" /><published>2026-03-16T00:00:00+00:00</published><updated>2026-03-16T00:00:00+00:00</updated><id>https://rhymion.github.io/blog/2026/03/16/nextjs-performance-remote-database</id><content type="html" xml:base="https://rhymion.github.io/blog/2026/03/16/nextjs-performance-remote-database/"><![CDATA[<h1 id="improving-nextjs-app-router-performance-with-a-remote-database">Improving Next.js App Router Performance with a Remote Database</h1>

<p>When your Next.js application connects to a database hosted in a distant region,
latency becomes the dominant factor in page load time. A single DB round-trip
might cost 500ms or more — and every sequential call stacks that cost visibly
for the user. This article walks through four techniques that, together, make
a significant difference in perceived and actual performance.</p>

<hr />

<h2 id="the-problem-sequential-waits-add-up">The Problem: Sequential Waits Add Up</h2>

<p>A typical data page in a Next.js App Router application does something like this:</p>

<ol>
  <li>Check who the current user is (auth lookup)</li>
  <li>Fetch the user’s permissions for this resource</li>
  <li>Fetch the actual data</li>
</ol>

<p>If each of these is a separate DB call and each costs 500ms, the user waits
1.5 seconds before seeing anything. On top of that, the entire HTML response is
held until all three complete — meaning the browser has nothing to render until
everything is done.</p>

<p>The following techniques address this at three levels: what the browser renders
first, which calls run in parallel, and how many calls happen at all.</p>

<hr />

<h2 id="technique-1-stream-content-with-suspense">Technique 1: Stream Content with Suspense</h2>

<h3 id="the-idea">The idea</h3>

<p>Next.js App Router supports streaming HTML responses. If you wrap an async
server component in a <code class="language-plaintext highlighter-rouge">&lt;Suspense&gt;</code> boundary, Next.js sends the outer HTML
shell to the browser immediately — without waiting for the data — then streams
in the content when it’s ready.</p>

<p>The key is to split every data-fetching page into two parts:</p>

<ul>
  <li>A <strong>sync outer component</strong> that returns instantly (no awaits, no DB calls)</li>
  <li>An <strong>async inner component</strong> that does the actual data fetching</li>
</ul>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// The outer component renders immediately — fast first byte</span>
<span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">ProductListPage</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">(</span>
    <span class="p">&lt;</span><span class="nc">Suspense</span> <span class="na">fallback</span><span class="p">=</span><span class="si">{</span><span class="p">&lt;</span><span class="nc">LoadingPlaceholder</span> <span class="p">/&gt;</span><span class="si">}</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nc">ProductListContent</span> <span class="p">/&gt;</span>
    <span class="p">&lt;/</span><span class="nc">Suspense</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>

<span class="c1">// The inner component streams in when data is ready</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">ProductListContent</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">products</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">getProducts</span><span class="p">();</span>
  <span class="k">return</span> <span class="p">&lt;</span><span class="nc">ProductTable</span> <span class="na">rows</span><span class="p">=</span><span class="si">{</span><span class="nx">products</span><span class="si">}</span> <span class="p">/&gt;;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Without this split, the page component itself is async, which blocks the
entire response. With it, the browser receives the page shell — including
layout, navigation, and the loading placeholder — at near-zero latency.
The content then streams in when the DB responds.</p>

<h3 id="pages-with-dynamic-route-parameters">Pages with dynamic route parameters</h3>

<p>For pages like <code class="language-plaintext highlighter-rouge">/products/[id]/edit</code>, the route params need to be awaited.
The cleanest pattern is to await params in the outer component and pass the
resolved values down:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">ProductEditPage</span><span class="p">({</span>
  <span class="nx">params</span><span class="p">,</span>
<span class="p">}:</span> <span class="p">{</span>
  <span class="nl">params</span><span class="p">:</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="kr">string</span> <span class="p">}</span><span class="o">&gt;</span><span class="p">;</span>
<span class="p">})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">id</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">params</span><span class="p">;</span>
  <span class="k">return</span> <span class="p">(</span>
    <span class="p">&lt;</span><span class="nc">Suspense</span> <span class="na">fallback</span><span class="p">=</span><span class="si">{</span><span class="p">&lt;</span><span class="nc">LoadingPlaceholder</span> <span class="p">/&gt;</span><span class="si">}</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nc">ProductEditContent</span> <span class="na">id</span><span class="p">=</span><span class="si">{</span><span class="nx">id</span><span class="si">}</span> <span class="p">/&gt;</span>
    <span class="p">&lt;/</span><span class="nc">Suspense</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="technique-2-show-skeleton-screens-while-loading">Technique 2: Show Skeleton Screens While Loading</h2>

<h3 id="the-idea-1">The idea</h3>

<p>Streaming Suspense gives the browser something to work with immediately, but
what does the user actually see while waiting? A blank area or a generic spinner
is disorienting. A skeleton screen — a low-fidelity outline of the content to
come — signals to the user exactly where the content will appear, making the
wait feel shorter.</p>

<p>MUI’s <code class="language-plaintext highlighter-rouge">&lt;Skeleton&gt;</code> component makes this straightforward:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">TableSkeleton</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">(</span>
    <span class="p">&lt;</span><span class="nc">Box</span> <span class="na">sx</span><span class="p">=&gt;</span>
      <span class="p">&lt;</span><span class="nc">Skeleton</span> <span class="na">variant</span><span class="p">=</span><span class="s">"rectangular"</span> <span class="na">height</span><span class="p">=</span><span class="si">{</span><span class="mi">52</span><span class="si">}</span> <span class="na">sx</span><span class="p">=</span> <span class="p">/&gt;</span>
      <span class="si">{</span><span class="p">[...</span><span class="nb">Array</span><span class="p">(</span><span class="mi">5</span><span class="p">)].</span><span class="nx">map</span><span class="p">((</span><span class="nx">_</span><span class="p">,</span> <span class="nx">i</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span>
        <span class="p">&lt;</span><span class="nc">Skeleton</span> <span class="na">key</span><span class="p">=</span><span class="si">{</span><span class="nx">i</span><span class="si">}</span> <span class="na">variant</span><span class="p">=</span><span class="s">"rectangular"</span> <span class="na">height</span><span class="p">=</span><span class="si">{</span><span class="mi">48</span><span class="si">}</span> <span class="na">sx</span><span class="p">=</span> <span class="p">/&gt;</span>
      <span class="p">))</span><span class="si">}</span>
    <span class="p">&lt;/</span><span class="nc">Box</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The skeleton matches the shape of the real content: a header row followed by
data rows. When the actual table streams in, there’s no layout shift — the
skeleton was already occupying the right space.</p>

<p>The same approach works for form pages:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">FormSkeleton</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">(</span>
    <span class="p">&lt;</span><span class="nc">Box</span> <span class="na">sx</span><span class="p">=&gt;</span>
      <span class="p">&lt;</span><span class="nc">Skeleton</span> <span class="na">variant</span><span class="p">=</span><span class="s">"rectangular"</span> <span class="na">width</span><span class="p">=</span><span class="si">{</span><span class="mi">200</span><span class="si">}</span> <span class="na">height</span><span class="p">=</span><span class="si">{</span><span class="mi">36</span><span class="si">}</span> <span class="na">sx</span><span class="p">=</span> <span class="p">/&gt;</span>
      <span class="si">{</span><span class="p">[...</span><span class="nb">Array</span><span class="p">(</span><span class="mi">4</span><span class="p">)].</span><span class="nx">map</span><span class="p">((</span><span class="nx">_</span><span class="p">,</span> <span class="nx">i</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span>
        <span class="p">&lt;</span><span class="nc">Skeleton</span> <span class="na">key</span><span class="p">=</span><span class="si">{</span><span class="nx">i</span><span class="si">}</span> <span class="na">variant</span><span class="p">=</span><span class="s">"rectangular"</span> <span class="na">height</span><span class="p">=</span><span class="si">{</span><span class="mi">56</span><span class="si">}</span> <span class="na">sx</span><span class="p">=</span> <span class="p">/&gt;</span>
      <span class="p">))</span><span class="si">}</span>
    <span class="p">&lt;/</span><span class="nc">Box</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Skeleton screens don’t reduce actual load time, but they meaningfully improve
perceived performance — which is what users notice.</p>

<hr />

<h2 id="technique-3-fetch-data-and-permissions-in-parallel">Technique 3: Fetch Data and Permissions in Parallel</h2>

<h3 id="the-problem-with-sequential-auth">The problem with sequential auth</h3>

<p>A common pattern for protected pages looks like this:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Three sequential round-trips</span>
<span class="kd">const</span> <span class="nx">userId</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">getCurrentUserId</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">permissions</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">getPermissions</span><span class="p">(</span><span class="nx">userId</span><span class="p">,</span> <span class="dl">'</span><span class="s1">product</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">products</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">getProducts</span><span class="p">();</span>
</code></pre></div></div>

<p>The permissions call depends on <code class="language-plaintext highlighter-rouge">userId</code>, so it can’t start until the first
call finishes. The data fetch is independent but happens last. On a remote DB,
this stacks three round-trips end-to-end.</p>

<h3 id="rethinking-the-auth-api">Rethinking the auth API</h3>

<p>The fix starts by reconsidering what the auth/permissions API returns. If the
function that resolves permissions also returns the <code class="language-plaintext highlighter-rouge">userId</code> it resolved, the
caller can eliminate the separate <code class="language-plaintext highlighter-rouge">getCurrentUserId()</code> call — and use
<code class="language-plaintext highlighter-rouge">Promise.all</code> to run permissions and data fetches concurrently:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// One round-trip for both</span>
<span class="kd">const</span> <span class="p">[{</span> <span class="nx">permissions</span><span class="p">,</span> <span class="nx">userId</span> <span class="p">},</span> <span class="nx">products</span><span class="p">]</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">([</span>
  <span class="nx">getPermissions</span><span class="p">(</span><span class="dl">'</span><span class="s1">product</span><span class="dl">'</span><span class="p">),</span>   <span class="c1">// now returns { permissions, userId } together</span>
  <span class="nx">getProducts</span><span class="p">(),</span>
<span class="p">]);</span>
</code></pre></div></div>

<p>For detail pages (where permissions may depend on the item’s creator or
assignee), the item and base permissions can still be fetched in parallel.
The item-level permission resolution happens afterward, once both are available:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">[</span><span class="nx">item</span><span class="p">,</span> <span class="p">{</span> <span class="na">permissions</span><span class="p">:</span> <span class="nx">basePermissions</span><span class="p">,</span> <span class="nx">userId</span> <span class="p">}]</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">([</span>
  <span class="nx">getProduct</span><span class="p">(</span><span class="nx">id</span><span class="p">),</span>
  <span class="nx">getPermissions</span><span class="p">(</span><span class="dl">'</span><span class="s1">product</span><span class="dl">'</span><span class="p">),</span>
<span class="p">]);</span>

<span class="c1">// Resolve item-specific permissions (creator/assignee rules) — no extra DB call</span>
<span class="kd">const</span> <span class="nx">finalPermissions</span> <span class="o">=</span> <span class="nx">resolveItemPermissions</span><span class="p">(</span><span class="nx">basePermissions</span><span class="p">,</span> <span class="nx">item</span><span class="p">,</span> <span class="nx">userId</span><span class="p">);</span>
</code></pre></div></div>

<p>This pattern turns what was three sequential DB calls into effectively one
parallel round-trip, saving two full DB latency cycles on every page load.</p>

<h3 id="keeping-component-interfaces-simple">Keeping component interfaces simple</h3>

<p>The internal permission object may carry rich context (general role, creator
role, assignee role) for server-side resolution. Components don’t need this
detail — they just need four booleans: can read, create, update, delete. Strip
the rich object down before passing it to components:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Server: resolve rich permissions</span>
<span class="kd">const</span> <span class="nx">finalPermissions</span> <span class="o">=</span> <span class="nx">resolveItemPermissions</span><span class="p">(</span><span class="nx">basePermissions</span><span class="p">,</span> <span class="nx">item</span><span class="p">,</span> <span class="nx">userId</span><span class="p">);</span>

<span class="c1">// Pass simplified flags to the component</span>
<span class="k">return</span> <span class="o">&lt;</span><span class="nx">ProductForm</span> <span class="nx">permissions</span><span class="o">=</span><span class="p">{</span><span class="nx">toSimpleFlags</span><span class="p">(</span><span class="nx">finalPermissions</span><span class="p">)}</span> <span class="sr">/&gt;</span><span class="err">;
</span></code></pre></div></div>

<p>This keeps the server logic flexible while keeping component props clean.</p>

<hr />

<h2 id="technique-4-avoid-unnecessary-cache-invalidation">Technique 4: Avoid Unnecessary Cache Invalidation</h2>

<p>Even after optimizing what happens on the first load, extra DB calls can sneak
in through cache invalidation logic. Two common sources:</p>

<h3 id="double-fetch-from-revalidatepath--redirect">Double-fetch from <code class="language-plaintext highlighter-rouge">revalidatePath</code> + <code class="language-plaintext highlighter-rouge">redirect</code></h3>

<p>In a Next.js Server Action, it’s tempting to both invalidate the cache and
redirect after a successful save:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">saveProduct</span><span class="p">(</span><span class="nx">data</span><span class="p">:</span> <span class="nx">FormData</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">upsertProduct</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
  <span class="nx">revalidatePath</span><span class="p">(</span><span class="dl">'</span><span class="s1">/products</span><span class="dl">'</span><span class="p">);</span>  <span class="c1">// invalidate</span>
  <span class="nx">redirect</span><span class="p">(</span><span class="dl">'</span><span class="s1">/products</span><span class="dl">'</span><span class="p">);</span>        <span class="c1">// navigate</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The problem is that <code class="language-plaintext highlighter-rouge">revalidatePath</code> schedules a background re-render of
<code class="language-plaintext highlighter-rouge">/products</code>, and <code class="language-plaintext highlighter-rouge">redirect</code> causes the client to navigate to <code class="language-plaintext highlighter-rouge">/products</code> —
which also triggers a render. The result is two DB calls for the list page
in rapid succession.</p>

<p>The fix: remove <code class="language-plaintext highlighter-rouge">revalidatePath</code>. The <code class="language-plaintext highlighter-rouge">redirect()</code> call in a Server Action
already invalidates the router cache for the destination, so <code class="language-plaintext highlighter-rouge">revalidatePath</code>
is redundant here:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">saveProduct</span><span class="p">(</span><span class="nx">data</span><span class="p">:</span> <span class="nx">FormData</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">upsertProduct</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
  <span class="nx">redirect</span><span class="p">(</span><span class="dl">'</span><span class="s1">/products</span><span class="dl">'</span><span class="p">);</span>  <span class="c1">// handles cache invalidation + navigation together</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="extra-fetch-from-routerrefresh-in-navigation-handlers">Extra fetch from <code class="language-plaintext highlighter-rouge">router.refresh()</code> in navigation handlers</h3>

<p>A similar issue appears in client-side navigation. It’s common to call both
<code class="language-plaintext highlighter-rouge">router.push()</code> and <code class="language-plaintext highlighter-rouge">router.refresh()</code> when a form’s back button is clicked:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Before: causes extra fetches</span>
<span class="kd">const</span> <span class="nx">handleBack</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">router</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="dl">'</span><span class="s1">/products</span><span class="dl">'</span><span class="p">);</span>
  <span class="nx">router</span><span class="p">.</span><span class="nx">refresh</span><span class="p">();</span>  <span class="c1">// triggers getProductDetail AND getProducts</span>
<span class="p">};</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">router.refresh()</code> re-renders the current page before navigation completes,
firing DB calls that are about to be discarded as the user leaves. Remove it:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// After: clean navigation, no extra fetches</span>
<span class="kd">const</span> <span class="nx">handleBack</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">router</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="dl">'</span><span class="s1">/products</span><span class="dl">'</span><span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>

<p>The destination page fetches its own fresh data when it mounts.</p>

<h3 id="when-revalidatepath-is-still-needed">When <code class="language-plaintext highlighter-rouge">revalidatePath</code> is still needed</h3>

<p>Some actions don’t redirect — they update the current page in place. Comment
CRUD, status toggles, and similar in-place updates fall into this category.
These still need <code class="language-plaintext highlighter-rouge">revalidatePath</code> because the client calls <code class="language-plaintext highlighter-rouge">router.refresh()</code>
to pick up changes, and the server needs its cache invalidated for that
refresh to return fresh data:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">addComment</span><span class="p">(</span><span class="nx">productId</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">text</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">createComment</span><span class="p">({</span> <span class="nx">productId</span><span class="p">,</span> <span class="nx">text</span> <span class="p">});</span>
  <span class="nx">revalidatePath</span><span class="p">(</span><span class="dl">'</span><span class="s1">/products</span><span class="dl">'</span><span class="p">);</span>  <span class="c1">// needed: no redirect, client will call router.refresh()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The rule: use <code class="language-plaintext highlighter-rouge">revalidatePath</code> when there is no <code class="language-plaintext highlighter-rouge">redirect</code>. Drop it when there is.</p>

<hr />

<h2 id="results-and-trade-offs">Results and Trade-offs</h2>

<p>These four techniques work at different layers and complement each other:</p>

<table>
  <thead>
    <tr>
      <th>Technique</th>
      <th>Latency saved</th>
      <th>Mechanism</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Streaming Suspense</td>
      <td>Fast TTFB regardless of DB latency</td>
      <td>Shell rendered before DB responds</td>
    </tr>
    <tr>
      <td>Skeleton screens</td>
      <td>Perceived wait reduced</td>
      <td>Visual placeholder during loading</td>
    </tr>
    <tr>
      <td>Parallel data + permissions</td>
      <td>~1–2× DB round-trips eliminated</td>
      <td><code class="language-plaintext highlighter-rouge">Promise.all</code> instead of sequential awaits</td>
    </tr>
    <tr>
      <td>Remove redundant invalidation</td>
      <td>1–2 extra renders eliminated per navigation</td>
      <td><code class="language-plaintext highlighter-rouge">redirect</code> instead of <code class="language-plaintext highlighter-rouge">revalidatePath</code> + <code class="language-plaintext highlighter-rouge">redirect</code></td>
    </tr>
  </tbody>
</table>

<p>The parallelism improvement (Technique 3) requires some thought about API
design — specifically making auth functions return enough context to avoid a
separate userId lookup. The cache invalidation improvements (Technique 4) are
mostly a matter of understanding what <code class="language-plaintext highlighter-rouge">revalidatePath</code> and <code class="language-plaintext highlighter-rouge">redirect</code> each do
and avoiding the overlap.</p>

<p>Streaming + skeletons are largely mechanical: split the page component, add
a <code class="language-plaintext highlighter-rouge">&lt;Suspense&gt;</code> boundary, provide a meaningful fallback. These changes don’t
require touching any business logic and can be applied incrementally.</p>

<p>Together, on an application with ~500ms DB round-trips, these changes can
reduce the wait before something interactive appears from several seconds to
near-zero (skeleton is immediate), and cut total DB calls on common navigation
patterns by half.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Improving Next.js App Router Performance with a Remote Database]]></summary></entry><entry><title type="html">Internationalization (i18n) — Locale-Based Routing with Next.js</title><link href="https://rhymion.github.io/blog/2026/03/07/i18n-locale-routing/" rel="alternate" type="text/html" title="Internationalization (i18n) — Locale-Based Routing with Next.js" /><published>2026-03-07T00:00:00+00:00</published><updated>2026-03-07T00:00:00+00:00</updated><id>https://rhymion.github.io/blog/2026/03/07/i18n-locale-routing</id><content type="html" xml:base="https://rhymion.github.io/blog/2026/03/07/i18n-locale-routing/"><![CDATA[<h1 id="internationalization-i18n--locale-based-routing-with-nextjs">Internationalization (i18n) — Locale-Based Routing with Next.js</h1>

<h2 id="overview">Overview</h2>

<p>In many cases apps must support multiple locales. We can use <strong>next-intl v4</strong> with <strong>locale-based URL routing</strong> in Next.js.
Every page URL is prefixed by the locale (<code class="language-plaintext highlighter-rouge">/en/booking</code>, <code class="language-plaintext highlighter-rouge">/ja/booking</code>), which gives:</p>

<ul>
  <li><strong>SEO</strong> — search engines index each locale at its own URL</li>
  <li><strong>CDN caching</strong> — responses are cacheable per locale without cookie-based variance</li>
  <li><strong>Shareable links</strong> — the locale is self-contained in the URL, not hidden in a cookie</li>
  <li><strong>Server-side rendering</strong> — the locale is known at request time without client-side detection</li>
</ul>

<p>In our case, we try to support the locales <code class="language-plaintext highlighter-rouge">en</code> (English, default) and <code class="language-plaintext highlighter-rouge">ja</code> (Japanese).</p>

<hr />

<h2 id="file-structure">File Structure</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>i18n/
  routing.ts          ← supported locales + default locale
  request.ts          ← server-side getRequestConfig (loads messages)
  navigation.ts       ← locale-aware Link, useRouter, usePathname, redirect

messages/
  en.json             ← English translation strings
  ja.json             ← Japanese translation strings

proxy.ts              ← Next.js middleware: locale routing + auth token check
app/
  layout.tsx          ← minimal root shell; reads locale via getLocale()
  [locale]/
    layout.tsx        ← locale layout: wraps NextIntlClientProvider + page shell
    @header/page.tsx  ← header with locale switcher (client component)
    @sidebar/page.tsx ← nav sidebar (client component)
    @footer/page.tsx  ← footer
    providers.tsx     ← SessionProvider + SidebarProvider
    login/page.tsx
    register/page.tsx
    [entity]/...      ← all generated entity pages
  api/                ← API routes (no locale prefix — unaffected by i18n)
</code></pre></div></div>

<hr />

<h2 id="key-files-explained">Key Files Explained</h2>

<h3 id="i18nroutingts"><code class="language-plaintext highlighter-rouge">i18n/routing.ts</code></h3>

<p>Defines the supported locales and the default. Import this wherever the locale list is needed (e.g. the header locale switcher, <code class="language-plaintext highlighter-rouge">generateStaticParams</code>).</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">defineRouting</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">next-intl/routing</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">routing</span> <span class="o">=</span> <span class="nx">defineRouting</span><span class="p">({</span>
  <span class="na">locales</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">en</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">ja</span><span class="dl">'</span><span class="p">],</span>
  <span class="na">defaultLocale</span><span class="p">:</span> <span class="dl">'</span><span class="s1">en</span><span class="dl">'</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="i18nrequestts"><code class="language-plaintext highlighter-rouge">i18n/request.ts</code></h3>

<p>Called by next-intl on every server request. Reads the locale from the URL (set by the middleware) and loads the matching message file.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">getRequestConfig</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">next-intl/server</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">routing</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./routing</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">getRequestConfig</span><span class="p">(</span><span class="k">async</span> <span class="p">({</span> <span class="nx">requestLocale</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="nx">locale</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">requestLocale</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">locale</span> <span class="o">||</span> <span class="o">!</span><span class="nx">routing</span><span class="p">.</span><span class="nx">locales</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">locale</span> <span class="k">as</span> <span class="dl">'</span><span class="s1">en</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">ja</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
    <span class="nx">locale</span> <span class="o">=</span> <span class="nx">routing</span><span class="p">.</span><span class="nx">defaultLocale</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="nx">locale</span><span class="p">,</span>
    <span class="na">messages</span><span class="p">:</span> <span class="p">(</span><span class="k">await</span> <span class="k">import</span><span class="p">(</span><span class="s2">`../messages/</span><span class="p">${</span><span class="nx">locale</span><span class="p">}</span><span class="s2">.json`</span><span class="p">)).</span><span class="k">default</span><span class="p">,</span>
  <span class="p">};</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="i18nnavigationts"><code class="language-plaintext highlighter-rouge">i18n/navigation.ts</code></h3>

<p>Re-exports locale-aware navigation helpers created by next-intl. <strong>Always import <code class="language-plaintext highlighter-rouge">Link</code>, <code class="language-plaintext highlighter-rouge">useRouter</code>, <code class="language-plaintext highlighter-rouge">usePathname</code>, and <code class="language-plaintext highlighter-rouge">redirect</code> from here</strong> instead of <code class="language-plaintext highlighter-rouge">next/link</code> or <code class="language-plaintext highlighter-rouge">next/navigation</code> — the wrappers automatically prepend the current locale to every path.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">createNavigation</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">next-intl/navigation</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">routing</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./routing</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">Link</span><span class="p">,</span> <span class="nx">redirect</span><span class="p">,</span> <span class="nx">usePathname</span><span class="p">,</span> <span class="nx">useRouter</span><span class="p">,</span> <span class="nx">getPathname</span> <span class="p">}</span> <span class="o">=</span>
  <span class="nx">createNavigation</span><span class="p">(</span><span class="nx">routing</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="proxyts-middleware"><code class="language-plaintext highlighter-rouge">proxy.ts</code> (middleware)</h3>

<p>The project uses <code class="language-plaintext highlighter-rouge">proxy.ts</code> as its middleware entry point. 
(While <code class="language-plaintext highlighter-rouge">middleware.ts</code> had been used, from Next.js 16 <code class="language-plaintext highlighter-rouge">proxy</code> is recommended instead). It chains two responsibilities:</p>

<ol>
  <li><strong>Locale routing</strong> — next-intl redirects bare paths (<code class="language-plaintext highlighter-rouge">/booking</code> → <code class="language-plaintext highlighter-rouge">/en/booking</code>) and sets the locale for the request</li>
  <li><strong>Auth protection</strong> — non-public paths require a valid JWT; unauthenticated users are redirected to <code class="language-plaintext highlighter-rouge">/{locale}/login</code></li>
</ol>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// proxy.ts (simplified)</span>
<span class="kd">const</span> <span class="nx">intlMiddleware</span> <span class="o">=</span> <span class="nx">createIntlMiddleware</span><span class="p">(</span><span class="nx">routing</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">PUBLIC_PATHS</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">/login</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">/register</span><span class="dl">'</span><span class="p">];</span>

<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">proxy</span><span class="p">(</span><span class="nx">req</span><span class="p">:</span> <span class="nx">NextRequest</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// determine path without locale prefix</span>
  <span class="kd">const</span> <span class="nx">isPublicPath</span> <span class="o">=</span> <span class="nx">PUBLIC_PATHS</span><span class="p">.</span><span class="nx">some</span><span class="p">(</span><span class="nx">p</span> <span class="o">=&gt;</span> <span class="nx">pathnameWithoutLocale</span> <span class="o">===</span> <span class="nx">p</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">intlResponse</span> <span class="o">=</span> <span class="nx">intlMiddleware</span><span class="p">(</span><span class="nx">req</span><span class="p">);</span>   <span class="c1">// handles locale redirect/rewrite</span>

  <span class="k">if</span> <span class="p">(</span><span class="nx">isPublicPath</span><span class="p">)</span> <span class="k">return</span> <span class="nx">intlResponse</span><span class="p">;</span>

  <span class="kd">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">getToken</span><span class="p">({</span> <span class="nx">req</span><span class="p">,</span> <span class="na">secret</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AUTH_SECRET</span> <span class="p">});</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">token</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">url</span><span class="p">.</span><span class="nx">pathname</span> <span class="o">=</span> <span class="s2">`/</span><span class="p">${</span><span class="nx">locale</span><span class="p">}</span><span class="s2">/login`</span><span class="p">;</span>
    <span class="k">return</span> <span class="nx">NextResponse</span><span class="p">.</span><span class="nx">redirect</span><span class="p">(</span><span class="nx">url</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">intlResponse</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">config</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">matcher</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">/((?!api|_next|_vercel|.*</span><span class="se">\\</span><span class="s1">..*).*)</span><span class="dl">'</span><span class="p">],</span>
<span class="p">};</span>
</code></pre></div></div>

<blockquote>
  <p><strong>Do not create <code class="language-plaintext highlighter-rouge">middleware.ts</code></strong> — the framework detects both files and throws an error.</p>
</blockquote>

<hr />

<h2 id="using-translations">Using Translations</h2>

<h3 id="in-server-components">In server components</h3>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">getTranslations</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">next-intl/server</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="k">default</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">MyPage</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">t</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">getTranslations</span><span class="p">(</span><span class="dl">'</span><span class="s1">Nav</span><span class="dl">'</span><span class="p">);</span>
  <span class="k">return</span> <span class="o">&lt;</span><span class="nx">h1</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">t</span><span class="p">(</span><span class="dl">'</span><span class="s1">home</span><span class="dl">'</span><span class="p">)}</span><span class="o">&lt;</span><span class="sr">/h1&gt;</span><span class="err">;
</span><span class="p">}</span>
</code></pre></div></div>

<h3 id="in-client-components">In client components</h3>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">"</span><span class="s2">use client</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">useTranslations</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">next-intl</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">MyComponent</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">t</span> <span class="o">=</span> <span class="nx">useTranslations</span><span class="p">(</span><span class="dl">'</span><span class="s1">Header</span><span class="dl">'</span><span class="p">);</span>
  <span class="k">return</span> <span class="p">&lt;</span><span class="nt">button</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">t</span><span class="p">(</span><span class="dl">'</span><span class="s1">signOut</span><span class="dl">'</span><span class="p">)</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;;</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p><strong>Note:</strong> <code class="language-plaintext highlighter-rouge">useTranslations</code> only works inside <code class="language-plaintext highlighter-rouge">NextIntlClientProvider</code>. The locale layout (<code class="language-plaintext highlighter-rouge">app/[locale]/layout.tsx</code>) wraps all pages in this provider, so any client component under <code class="language-plaintext highlighter-rouge">[locale]/</code> can call <code class="language-plaintext highlighter-rouge">useTranslations</code>.</p>
</blockquote>

<h3 id="in-applocalelayouttsx">In <code class="language-plaintext highlighter-rouge">app/[locale]/layout.tsx</code></h3>

<p>The locale layout must call <code class="language-plaintext highlighter-rouge">setRequestLocale</code> (for static rendering support) and fetch messages before rendering:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">NextIntlClientProvider</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">next-intl</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">getMessages</span><span class="p">,</span> <span class="nx">setRequestLocale</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">next-intl/server</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="k">default</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">LocaleLayout</span><span class="p">({</span> <span class="nx">children</span><span class="p">,</span> <span class="nx">params</span> <span class="p">})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">locale</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">params</span><span class="p">;</span>
  <span class="nx">setRequestLocale</span><span class="p">(</span><span class="nx">locale</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">messages</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">getMessages</span><span class="p">();</span>

  <span class="k">return</span> <span class="p">(</span>
    <span class="p">&lt;</span><span class="nc">NextIntlClientProvider</span> <span class="na">messages</span><span class="p">=</span><span class="si">{</span><span class="nx">messages</span><span class="si">}</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nc">Providers</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nc">Providers</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nc">NextIntlClientProvider</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="adding-a-new-locale">Adding a New Locale</h2>

<ol>
  <li>Add the locale to <code class="language-plaintext highlighter-rouge">i18n/routing.ts</code>:
    <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">locales</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">en</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">ja</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">fr</span><span class="dl">'</span><span class="p">],</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Create the message file <code class="language-plaintext highlighter-rouge">messages/fr.json</code> with all required keys (copy <code class="language-plaintext highlighter-rouge">en.json</code> as a starting template).</p>
  </li>
  <li>Update the <code class="language-plaintext highlighter-rouge">localeLabels</code> map in <code class="language-plaintext highlighter-rouge">app/[locale]/@header/page.tsx</code>:
    <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">localeLabels</span><span class="p">:</span> <span class="nb">Record</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="kr">string</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">en</span><span class="p">:</span> <span class="dl">"</span><span class="s2">EN</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">ja</span><span class="p">:</span> <span class="dl">"</span><span class="s2">日本語</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">fr</span><span class="p">:</span> <span class="dl">"</span><span class="s2">FR</span><span class="dl">"</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div>    </div>
  </li>
  <li>Update <code class="language-plaintext highlighter-rouge">i18n/request.ts</code> — widen the type guard to include the new locale:
    <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">locale</span> <span class="o">||</span> <span class="o">!</span><span class="nx">routing</span><span class="p">.</span><span class="nx">locales</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">locale</span> <span class="k">as</span> <span class="dl">'</span><span class="s1">en</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">ja</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">fr</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
</code></pre></div>    </div>
  </li>
</ol>

<p>That’s all — next-intl handles the rest automatically.</p>

<hr />

<h3 id="switching-locale">Switching locale</h3>

<p>From a client component, use <code class="language-plaintext highlighter-rouge">useRouter().replace</code> with the <code class="language-plaintext highlighter-rouge">locale</code> option:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">router</span> <span class="o">=</span> <span class="nx">useRouter</span><span class="p">();</span>     <span class="c1">// from @/i18n/navigation</span>
<span class="kd">const</span> <span class="nx">pathname</span> <span class="o">=</span> <span class="nx">usePathname</span><span class="p">();</span> <span class="c1">// from @/i18n/navigation</span>

<span class="nx">router</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="nx">pathname</span><span class="p">,</span> <span class="p">{</span> <span class="na">locale</span><span class="p">:</span> <span class="dl">'</span><span class="s1">ja</span><span class="dl">'</span> <span class="p">});</span>
</code></pre></div></div>

<p>The header already exposes this as labelled buttons (EN / 日本語).</p>

<hr />

<h2 id="message-file-structure">Message File Structure</h2>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"Header"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"signIn"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sign In"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"signOut"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sign Out"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"openMenu"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Open menu"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"closeMenu"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Close menu"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"Nav"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"home"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Home"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"dbTables"</span><span class="p">:</span><span class="w"> </span><span class="s2">"DB Tables"</span><span class="p">,</span><span class="w">
    </span><span class="err">...</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"Auth"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"signInTitle"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sign in to your account"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"registerTitle"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Create your account"</span><span class="p">,</span><span class="w">
    </span><span class="err">...</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Add new namespaces (top-level keys) as features grow. Keep key names camelCase and scoped to their UI context.</p>

<hr />

<h2 id="testing">Testing</h2>

<h3 id="cypress-e2e">Cypress E2E</h3>

<p>All <code class="language-plaintext highlighter-rouge">cy.visit()</code> calls use the <code class="language-plaintext highlighter-rouge">/en/</code> prefix (the default locale):</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">cy</span><span class="p">.</span><span class="nx">visit</span><span class="p">(</span><span class="dl">'</span><span class="s1">/en/booking</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">visit</span><span class="p">(</span><span class="dl">'</span><span class="s1">/en/booking/new</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">visit</span><span class="p">(</span><span class="dl">'</span><span class="s1">/en/</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>

<p>This is baked into the test templates (<code class="language-plaintext highlighter-rouge">utils/scripts/templates-test.ts</code>). Regenerating tests will produce the correct paths automatically.</p>

<h3 id="vitest-unit-tests">Vitest unit tests</h3>

<p>NextIntlClientProvider is also needed in (component) testing, when the target component uses translation. 
Unlike in components, if it is acceptable to hard-code locale in testing, code is slightly simpler.</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">render</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@testing-library/react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">NextIntlClientProvider</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">next-intl</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">messages</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@/messages/en.json</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nx">renderWithIntl</span><span class="p">(</span><span class="nx">ui</span><span class="p">:</span> <span class="nx">React</span><span class="p">.</span><span class="nx">ReactElement</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">render</span><span class="p">(</span>
    <span class="p">&lt;</span><span class="nc">NextIntlClientProvider</span> <span class="na">locale</span><span class="p">=</span><span class="s">"en"</span> <span class="na">messages</span><span class="p">=</span><span class="si">{</span><span class="nx">messages</span><span class="si">}</span><span class="p">&gt;</span>
      <span class="si">{</span><span class="nx">ui</span><span class="si">}</span>
    <span class="p">&lt;/</span><span class="nc">NextIntlClientProvider</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We can now call the target component with <code class="language-plaintext highlighter-rouge">renderWithIntl</code> as wrapper. 
Of course we can also use mock instead, if i18n itself is not the target of testing.</p>

<hr />

<h3 id="additional-resources">Additional Resources</h3>

<ul>
  <li>📚 <a href="https://nextjs.org/docs/app/guides/internationalization">Next.js (app router) internationalization documentation</a></li>
  <li>🔧 <a href="https://nextjs.org/docs/pages/guides/internationalization">Next.js (pages router) internationalization documentation</a></li>
</ul>

<hr />

<p><em>Tags: Next.js, i18n, Testing, E2E Testing, Cypress, TypeScript</em></p>]]></content><author><name></name></author><summary type="html"><![CDATA[Internationalization (i18n) — Locale-Based Routing with Next.js]]></summary></entry><entry><title type="html">How to Use Prisma Adapters for Testing While Keeping Accelerate for Production</title><link href="https://rhymion.github.io/blog/2026/02/17/prisma-adapter-testing-strategy/" rel="alternate" type="text/html" title="How to Use Prisma Adapters for Testing While Keeping Accelerate for Production" /><published>2026-02-17T00:00:00+00:00</published><updated>2026-02-17T00:00:00+00:00</updated><id>https://rhymion.github.io/blog/2026/02/17/prisma-adapter-testing-strategy</id><content type="html" xml:base="https://rhymion.github.io/blog/2026/02/17/prisma-adapter-testing-strategy/"><![CDATA[<h1 id="how-to-use-prisma-adapters-for-testing-while-keeping-accelerate-for-production">How to Use Prisma Adapters for Testing While Keeping Accelerate for Production</h1>

<table>
  <tbody>
    <tr>
      <td><strong>Published:</strong> February 17, 2026</td>
      <td><strong>Reading Time:</strong> 12 minutes</td>
      <td><strong>Difficulty:</strong> Intermediate</td>
    </tr>
  </tbody>
</table>

<hr />

<p>Are you struggling to test your Next.js application locally while using Prisma Accelerate in production? You’re not alone. Many developers face the challenge of managing database connections across different environments when using Prisma’s powerful Accelerate extension.</p>

<p>In this comprehensive guide, you’ll learn how to set up a flexible database strategy that uses Prisma Accelerate for production while seamlessly switching to direct connections for local development and testing.</p>

<hr />

<h2 id="-what-youll-learn">📚 What You’ll Learn</h2>

<p>By the end of this tutorial, you will:</p>

<ul>
  <li>✅ Understand why Prisma Accelerate creates challenges for local testing</li>
  <li>✅ Set up conditional database adapters for different environments</li>
  <li>✅ Configure your Next.js app to use Accelerate in production and direct connections for testing</li>
  <li>✅ Implement a complete e2e testing workflow with Docker</li>
  <li>✅ Optimize bundle size by dynamically importing adapters</li>
  <li>✅ Avoid common pitfalls when working with Prisma 7</li>
</ul>

<hr />

<h2 id="understanding-the-problem-why-accelerate-breaks-local-testing">Understanding the Problem: Why Accelerate Breaks Local Testing</h2>

<h3 id="what-is-prisma-accelerate">What is Prisma Accelerate?</h3>

<p><a href="https://www.prisma.io/docs/accelerate">Prisma Accelerate</a> is a cloud-based connection pooler and global cache for your database. It provides:</p>

<ul>
  <li>🚀 <strong>Global edge caching</strong> for faster query responses</li>
  <li>🔄 <strong>Connection pooling</strong> to handle thousands of serverless connections</li>
  <li>📊 <strong>Built-in performance monitoring</strong> and analytics</li>
</ul>

<h3 id="the-testing-dilemma">The Testing Dilemma</h3>

<p>When you install <code class="language-plaintext highlighter-rouge">@prisma/extension-accelerate</code>, it modifies PrismaClient’s TypeScript types to <strong>require</strong> the <code class="language-plaintext highlighter-rouge">accelerateUrl</code> parameter:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ Works in production with Accelerate</span>
<span class="kd">const</span> <span class="nx">prisma</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PrismaClient</span><span class="p">({</span>
  <span class="na">accelerateUrl</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PRISMA_DATABASE_URL</span><span class="p">,</span> <span class="c1">// prisma+postgres://...</span>
<span class="p">}).</span><span class="nx">$extends</span><span class="p">(</span><span class="nx">withAccelerate</span><span class="p">());</span>
</code></pre></div></div>

<p>But here’s the catch – the extension validates URLs at runtime and <strong>only accepts</strong> URLs starting with <code class="language-plaintext highlighter-rouge">prisma://</code> or <code class="language-plaintext highlighter-rouge">prisma+postgres://</code>:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ Fails with local database!</span>
<span class="kd">const</span> <span class="nx">prisma</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PrismaClient</span><span class="p">({</span>
  <span class="na">accelerateUrl</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">DATABASE_URL</span><span class="p">,</span> <span class="c1">// postgresql://localhost:5432/...</span>
<span class="p">}).</span><span class="nx">$extends</span><span class="p">(</span><span class="nx">withAccelerate</span><span class="p">());</span>

<span class="c1">// Error: InvalidDatasourceError: the URL must start with prisma:// or prisma+postgres://</span>
</code></pre></div></div>

<p>This creates a frustrating situation where you can’t easily test your application locally without:</p>
<ul>
  <li>Creating fake Accelerate URLs (hacky)</li>
  <li>Paying for Accelerate connections during testing (wasteful)</li>
  <li>Removing the extension entirely (losing production benefits)</li>
</ul>

<blockquote>
  <p>💡 <strong>Expert Tip:</strong> This issue only affects projects using <code class="language-plaintext highlighter-rouge">@prisma/extension-accelerate</code>. If you’re not using Accelerate, you can safely use direct database connections everywhere.</p>
</blockquote>

<hr />

<h2 id="the-solution-conditional-adapter-strategy">The Solution: Conditional Adapter Strategy</h2>

<p>The elegant solution is to use <strong>database adapters</strong> for local/test environments while keeping Accelerate for production. This gives you the best of both worlds.</p>

<h3 id="how-it-works">How It Works</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">PrismaClient</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@/app/generated/prisma/client</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">withAccelerate</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@prisma/extension-accelerate</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">createPrismaClient</span> <span class="o">=</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PRISMA_DATABASE_URL</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Production: Use Prisma Accelerate</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Using Accelerate URL for Prisma Client</span><span class="dl">'</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PrismaClient</span><span class="p">({</span>
      <span class="na">accelerateUrl</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PRISMA_DATABASE_URL</span><span class="p">,</span>
      <span class="na">log</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">development</span><span class="dl">'</span> <span class="p">?</span> <span class="p">[</span><span class="dl">'</span><span class="s1">query</span><span class="dl">'</span><span class="p">]</span> <span class="p">:</span> <span class="p">[],</span>
    <span class="p">}).</span><span class="nx">$extends</span><span class="p">(</span><span class="nx">withAccelerate</span><span class="p">());</span>
    <span class="k">return</span> <span class="nx">client</span><span class="p">;</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="c1">// Development/Testing: Use direct connection with adapter</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Using direct database connection for Prisma Client</span><span class="dl">'</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">connectionString</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">DATABASE_URL</span><span class="p">;</span>
    
    <span class="c1">// Dynamic import to avoid bundling adapter in production</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">PrismaPg</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">@prisma/adapter-pg</span><span class="dl">'</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">adapter</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PrismaPg</span><span class="p">({</span> <span class="nx">connectionString</span> <span class="p">});</span>
    <span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PrismaClient</span><span class="p">({</span> <span class="nx">adapter</span> <span class="p">});</span>
    <span class="k">return</span> <span class="nx">client</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">};</span>

<span class="kd">const</span> <span class="nx">prisma</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">createPrismaClient</span><span class="p">();</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">prisma</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>How the conditional logic works:</strong></p>

<table>
  <thead>
    <tr>
      <th>Environment</th>
      <th><code class="language-plaintext highlighter-rouge">PRISMA_DATABASE_URL</code> Set?</th>
      <th>Connection Type</th>
      <th>Extension Used</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Production</td>
      <td>✅ Yes</td>
      <td>Accelerate</td>
      <td><code class="language-plaintext highlighter-rouge">withAccelerate()</code></td>
    </tr>
    <tr>
      <td>Development</td>
      <td>❌ No</td>
      <td>Direct (adapter)</td>
      <td>None</td>
    </tr>
    <tr>
      <td>Testing</td>
      <td>❌ No</td>
      <td>Direct (adapter)</td>
      <td>None</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="step-by-step-implementation-guide">Step-by-Step Implementation Guide</h2>

<h3 id="prerequisites">Prerequisites</h3>

<p>Before you begin, make sure you have:</p>

<ul>
  <li>Node.js 18+ installed</li>
  <li>A Next.js project with Prisma configured</li>
  <li>Prisma Accelerate set up (optional, for production)</li>
  <li>Docker installed (for local testing database)</li>
</ul>

<hr />

<h3 id="step-1-install-the-postgresql-adapter">Step 1: Install the PostgreSQL Adapter</h3>

<p>Install <code class="language-plaintext highlighter-rouge">@prisma/adapter-pg</code> as a <strong>development dependency</strong>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> <span class="nt">--save-dev</span> @prisma/adapter-pg
</code></pre></div></div>

<blockquote>
  <p>⚠️ <strong>Important:</strong> Install as a dev dependency (<code class="language-plaintext highlighter-rouge">--save-dev</code>) to prevent bundling it in production builds.</p>
</blockquote>

<hr />

<h3 id="step-2-set-up-environment-variables">Step 2: Set Up Environment Variables</h3>

<p>Create separate environment files for different scenarios:</p>

<h4 id="envlocal-development-with-production-database"><code class="language-plaintext highlighter-rouge">.env.local</code> (Development with Production Database)</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Direct connection for Prisma migrations</span>
<span class="nv">DATABASE_URL</span><span class="o">=</span><span class="s2">"postgres://user:pass@db.prisma.io:5432/production_db"</span>

<span class="c"># Accelerate URL for application runtime</span>
<span class="nv">PRISMA_DATABASE_URL</span><span class="o">=</span><span class="s2">"prisma+postgres://accelerate.prisma-data.net/?api_key=eyJhbG..."</span>

<span class="c"># NextAuth configuration</span>
<span class="nv">AUTH_SECRET</span><span class="o">=</span><span class="s2">"your-production-secret-key"</span>
</code></pre></div></div>

<h4 id="envtest-local-testing"><code class="language-plaintext highlighter-rouge">.env.test</code> (Local Testing)</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Local test database - NO PRISMA_DATABASE_URL here!</span>
<span class="nv">DATABASE_URL</span><span class="o">=</span><span class="s2">"postgresql://postgres:postgres@localhost:5432/myapp_test"</span>

<span class="c"># NextAuth configuration</span>
<span class="nv">AUTH_SECRET</span><span class="o">=</span><span class="s2">"test-secret-key-for-testing"</span>
<span class="nv">NODE_ENV</span><span class="o">=</span><span class="s2">"test"</span>
</code></pre></div></div>

<blockquote>
  <p>💡 <strong>Pro Tip:</strong> The key is to <strong>omit</strong> <code class="language-plaintext highlighter-rouge">PRISMA_DATABASE_URL</code> in test environments. This triggers the adapter-based connection instead of Accelerate.</p>
</blockquote>

<hr />

<h3 id="step-3-update-your-prisma-client-configuration">Step 3: Update Your Prisma Client Configuration</h3>

<p>Create or update your <code class="language-plaintext highlighter-rouge">lib/prisma.ts</code> file:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">PrismaClient</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@/app/generated/prisma/client</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">withAccelerate</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@prisma/extension-accelerate</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">globalForPrisma</span> <span class="o">=</span> <span class="nb">global</span> <span class="k">as</span> <span class="nx">unknown</span> <span class="k">as</span> <span class="p">{</span> <span class="na">prisma</span><span class="p">:</span> <span class="nx">PrismaClient</span> <span class="p">};</span>

<span class="kd">const</span> <span class="nx">createPrismaClient</span> <span class="o">=</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PRISMA_DATABASE_URL</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Production path: Accelerate</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Using Accelerate URL for Prisma Client</span><span class="dl">'</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PrismaClient</span><span class="p">({</span>
      <span class="na">accelerateUrl</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PRISMA_DATABASE_URL</span><span class="p">,</span>
      <span class="na">log</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">development</span><span class="dl">'</span> <span class="p">?</span> <span class="p">[</span><span class="dl">'</span><span class="s1">query</span><span class="dl">'</span><span class="p">]</span> <span class="p">:</span> <span class="p">[],</span>
    <span class="p">}).</span><span class="nx">$extends</span><span class="p">(</span><span class="nx">withAccelerate</span><span class="p">());</span>
    <span class="k">return</span> <span class="nx">client</span><span class="p">;</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="c1">// Development/Test path: Direct connection</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Using direct database connection for Prisma Client</span><span class="dl">'</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">connectionString</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">DATABASE_URL</span> <span class="k">as</span> <span class="kr">string</span><span class="p">;</span>
    
    <span class="c1">// Dynamic import - only loaded when needed</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">PrismaPg</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">@prisma/adapter-pg</span><span class="dl">'</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">adapter</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PrismaPg</span><span class="p">({</span> <span class="nx">connectionString</span> <span class="p">});</span>
    <span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PrismaClient</span><span class="p">({</span> <span class="nx">adapter</span> <span class="p">});</span>
    <span class="k">return</span> <span class="nx">client</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">};</span>

<span class="kd">const</span> <span class="nx">prisma</span> <span class="o">=</span> <span class="nx">globalForPrisma</span><span class="p">.</span><span class="nx">prisma</span> <span class="o">||</span> <span class="k">await</span> <span class="nx">createPrismaClient</span><span class="p">();</span>

<span class="k">if</span> <span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">!==</span> <span class="dl">"</span><span class="s2">production</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">globalForPrisma</span><span class="p">.</span><span class="nx">prisma</span> <span class="o">=</span> <span class="nx">prisma</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">prisma</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>Key implementation details:</strong></p>

<ol>
  <li><strong>Async function</strong> - Required for dynamic imports</li>
  <li><strong>Dynamic import</strong> - <code class="language-plaintext highlighter-rouge">await import('@prisma/adapter-pg')</code> loads the adapter only when needed</li>
  <li><strong>Type assertion</strong> - Ensures TypeScript knows the connection string exists</li>
  <li><strong>Singleton pattern</strong> - Prevents multiple Prisma instances in development</li>
</ol>

<hr />

<h3 id="step-4-configure-prisma-7">Step 4: Configure Prisma 7</h3>

<p>If you’re using Prisma 7, the configuration structure has changed significantly.</p>

<h4 id="prismaschemaprisma"><code class="language-plaintext highlighter-rouge">prisma/schema.prisma</code></h4>

<pre><code class="language-prisma">datasource db {
  provider = "postgresql"
  // ⚠️ No 'url' property in Prisma 7!
}

generator client {
  provider = "prisma-client"
  output   = "../app/generated/prisma"
}

model User {
  id       String  @id @default(cuid())
  email    String  @unique
  password String
  // ... other fields
}
</code></pre>

<h4 id="prismaconfigts"><code class="language-plaintext highlighter-rouge">prisma.config.ts</code></h4>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="dl">"</span><span class="s2">dotenv/config</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">prisma/config</span><span class="dl">"</span><span class="p">;</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
  <span class="na">schema</span><span class="p">:</span> <span class="dl">"</span><span class="s2">prisma/schema.prisma</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">datasource</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">url</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">DATABASE_URL</span><span class="p">,</span> <span class="c1">// Used for migrations</span>
  <span class="p">},</span>
<span class="p">});</span>
</code></pre></div></div>

<blockquote>
  <p>📌 <strong>Remember:</strong> In Prisma 7, the <code class="language-plaintext highlighter-rouge">url</code> property moved from <code class="language-plaintext highlighter-rouge">schema.prisma</code> to <code class="language-plaintext highlighter-rouge">prisma.config.ts</code>. This affects migrations and introspection but not runtime connections.</p>
</blockquote>

<hr />

<h2 id="setting-up-your-e2e-testing-environment">Setting Up Your E2E Testing Environment</h2>

<p>Now let’s set up a complete testing workflow using Docker and Cypress.</p>

<h3 id="step-5-create-docker-compose-configuration">Step 5: Create Docker Compose Configuration</h3>

<p>Create <code class="language-plaintext highlighter-rouge">docker-compose.test.yml</code> in your project root:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.8'</span>

<span class="na">services</span><span class="pi">:</span>
  <span class="na">postgres-test</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">postgres:16-alpine</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">myapp-test-db</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">POSTGRES_USER</span><span class="pi">:</span> <span class="s">postgres</span>
      <span class="na">POSTGRES_PASSWORD</span><span class="pi">:</span> <span class="s">postgres</span>
      <span class="na">POSTGRES_DB</span><span class="pi">:</span> <span class="s">myapp_test</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">5432:5432"</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">postgres-test-data:/var/lib/postgresql/data</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD-SHELL"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">pg_isready</span><span class="nv"> </span><span class="s">-U</span><span class="nv"> </span><span class="s">postgres"</span><span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">10s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">5</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">postgres-test-data</span><span class="pi">:</span>
    <span class="na">driver</span><span class="pi">:</span> <span class="s">local</span>
</code></pre></div></div>

<hr />

<h3 id="step-6-add-npm-scripts">Step 6: Add NPM Scripts</h3>

<p>Update your <code class="language-plaintext highlighter-rouge">package.json</code>:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"dev"</span><span class="p">:</span><span class="w"> </span><span class="s2">"next dev"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"dev:test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"dotenv -e .env.test -- next dev"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"next build"</span><span class="p">,</span><span class="w">
    
    </span><span class="nl">"docker:test:up"</span><span class="p">:</span><span class="w"> </span><span class="s2">"docker-compose -f docker-compose.test.yml up -d"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"docker:test:down"</span><span class="p">:</span><span class="w"> </span><span class="s2">"docker-compose -f docker-compose.test.yml down"</span><span class="p">,</span><span class="w">
    
    </span><span class="nl">"db:reset:test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"dotenv -e .env.test -- prisma migrate reset --force"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"db:seed:test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"dotenv -e .env.test -- tsx scripts/seed-test-db.ts"</span><span class="p">,</span><span class="w">
    
    </span><span class="nl">"cy:open"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cypress open"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"cy:run"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cypress run --browser chromium"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"cy:test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"start-server-and-test dev:test http://localhost:3000 cy:run"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Required dependencies:</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> <span class="nt">--save-dev</span> dotenv-cli start-server-and-test tsx
</code></pre></div></div>

<hr />

<h3 id="step-7-create-database-helper-functions">Step 7: Create Database Helper Functions</h3>

<p>Create <code class="language-plaintext highlighter-rouge">cypress/support/db-helpers.ts</code>:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">PrismaClient</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@/app/generated/prisma/client</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">TEST_CREDENTIALS</span><span class="p">,</span> <span class="nx">getTestPasswordHash</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./test-credentials</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// Simple direct connection - no Accelerate needed</span>
<span class="kd">const</span> <span class="nx">prisma</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PrismaClient</span><span class="p">();</span>

<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">resetTestDatabase</span><span class="p">()</span> <span class="p">{</span>
  <span class="c1">// Delete all records in reverse order (respect foreign keys)</span>
  <span class="k">await</span> <span class="nx">prisma</span><span class="p">.</span><span class="nx">post</span><span class="p">.</span><span class="nx">deleteMany</span><span class="p">();</span>
  <span class="k">await</span> <span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">deleteMany</span><span class="p">();</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">seedTestDatabase</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">hashedPassword</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">getTestPasswordHash</span><span class="p">();</span>
  
  <span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span>
    <span class="na">data</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">email</span><span class="p">:</span> <span class="nx">TEST_CREDENTIALS</span><span class="p">.</span><span class="nx">email</span><span class="p">,</span>
      <span class="na">name</span><span class="p">:</span> <span class="nx">TEST_CREDENTIALS</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
      <span class="na">password</span><span class="p">:</span> <span class="nx">hashedPassword</span><span class="p">,</span>
    <span class="p">},</span>
  <span class="p">});</span>

  <span class="k">return</span> <span class="p">{</span> <span class="nx">user</span> <span class="p">};</span>
<span class="p">}</span>

<span class="k">export</span> <span class="p">{</span> <span class="nx">prisma</span> <span class="p">};</span>
</code></pre></div></div>

<blockquote>
  <p>💡 <strong>Note:</strong> Test helpers use <code class="language-plaintext highlighter-rouge">new PrismaClient()</code> without parameters because <code class="language-plaintext highlighter-rouge">.env.test</code> only has <code class="language-plaintext highlighter-rouge">DATABASE_URL</code>, triggering the adapter path automatically.</p>
</blockquote>

<hr />

<h3 id="step-8-configure-cypress">Step 8: Configure Cypress</h3>

<p>Update <code class="language-plaintext highlighter-rouge">cypress.config.ts</code>:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">cypress</span><span class="dl">"</span><span class="p">;</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
  <span class="na">e2e</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">baseUrl</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://localhost:3000</span><span class="dl">"</span><span class="p">,</span>
    <span class="nx">setupNodeEvents</span><span class="p">(</span><span class="nx">on</span><span class="p">,</span> <span class="nx">config</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">dotenv</span><span class="dl">'</span><span class="p">).</span><span class="nx">config</span><span class="p">({</span> <span class="na">path</span><span class="p">:</span> <span class="dl">'</span><span class="s1">.env.test</span><span class="dl">'</span> <span class="p">});</span>
      
      <span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">task</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
        <span class="k">async</span> <span class="dl">'</span><span class="s1">db:reset</span><span class="dl">'</span><span class="p">()</span> <span class="p">{</span>
          <span class="kd">const</span> <span class="p">{</span> <span class="nx">resetTestDatabase</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./cypress/support/db-helpers</span><span class="dl">'</span><span class="p">);</span>
          <span class="k">await</span> <span class="nx">resetTestDatabase</span><span class="p">();</span>
          <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
        <span class="p">},</span>
        <span class="k">async</span> <span class="dl">'</span><span class="s1">db:seed</span><span class="dl">'</span><span class="p">()</span> <span class="p">{</span>
          <span class="kd">const</span> <span class="p">{</span> <span class="nx">seedTestDatabase</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./cypress/support/db-helpers</span><span class="dl">'</span><span class="p">);</span>
          <span class="k">await</span> <span class="nx">seedTestDatabase</span><span class="p">();</span>
          <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
        <span class="p">}</span>
      <span class="p">});</span>
      
      <span class="k">return</span> <span class="nx">config</span><span class="p">;</span>
    <span class="p">},</span>
    <span class="na">video</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
  <span class="p">},</span>
<span class="p">});</span>
</code></pre></div></div>

<hr />

<h2 id="your-complete-testing-workflow">Your Complete Testing Workflow</h2>

<p>Now you have everything set up. Here’s how to run tests:</p>

<h3 id="quick-start">Quick Start</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. Start the test database</span>
npm run docker:test:up

<span class="c"># 2. Initialize the database schema</span>
npm run db:reset:test

<span class="c"># 3. Run e2e tests (auto-starts dev server)</span>
npm run cy:test
</code></pre></div></div>

<h3 id="manual-testing-with-live-dev-server">Manual Testing (with live dev server)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Terminal 1: Start dev server with test database</span>
npm run dev:test

<span class="c"># Terminal 2: Run Cypress interactively</span>
npm run cy:open
</code></pre></div></div>

<h3 id="daily-development-workflow">Daily Development Workflow</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Development with production database</span>
npm run dev

<span class="c"># Testing</span>
npm run cy:test

<span class="c"># Stop test database when done</span>
npm run docker:test:down
</code></pre></div></div>

<hr />

<h2 id="benefits-of-this-approach">Benefits of This Approach</h2>

<h3 id="-1-environment-parity">🎯 1. Environment Parity</h3>

<p>Each environment uses the optimal connection strategy:</p>
<ul>
  <li><strong>Production:</strong> Accelerate for global caching and pooling</li>
  <li><strong>Testing:</strong> Local database for speed and isolation</li>
</ul>

<h3 id="-2-cost-efficiency">💰 2. Cost Efficiency</h3>

<p>You don’t pay for Accelerate connections during development and testing.</p>

<h3 id="-3-performance">⚡ 3. Performance</h3>

<p>Local database connections are significantly faster than going through Accelerate’s cloud infrastructure for tests.</p>

<h3 id="-4-bundle-size-optimization">📦 4. Bundle Size Optimization</h3>

<p>The adapter is dynamically imported, so it’s never included in your production bundle.</p>

<h3 id="-5-easy-environment-switching">🔄 5. Easy Environment Switching</h3>

<p>Change environments by simply setting or unsetting <code class="language-plaintext highlighter-rouge">PRISMA_DATABASE_URL</code>.</p>

<hr />

<h2 id="common-pitfalls-and-how-to-avoid-them">Common Pitfalls and How to Avoid Them</h2>

<h3 id="-pitfall-1-top-level-adapter-import">❌ Pitfall 1: Top-Level Adapter Import</h3>

<p><strong>Wrong:</strong></p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">PrismaPg</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@prisma/adapter-pg</span><span class="dl">'</span><span class="p">;</span> <span class="c1">// Always bundled!</span>
</code></pre></div></div>

<p><strong>Correct:</strong></p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span> <span class="nx">PrismaPg</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">@prisma/adapter-pg</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// Only when needed</span>
</code></pre></div></div>

<hr />

<h3 id="-pitfall-2-setting-prisma_database_url-in-test-environment">❌ Pitfall 2: Setting PRISMA_DATABASE_URL in Test Environment</h3>

<p><strong>Wrong:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># .env.test</span>
<span class="nv">DATABASE_URL</span><span class="o">=</span><span class="s2">"postgresql://localhost:5432/test"</span>
<span class="nv">PRISMA_DATABASE_URL</span><span class="o">=</span><span class="s2">"postgresql://localhost:5432/test"</span> <span class="c"># Don't do this!</span>
</code></pre></div></div>

<p><strong>Correct:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># .env.test</span>
<span class="nv">DATABASE_URL</span><span class="o">=</span><span class="s2">"postgresql://localhost:5432/test"</span>
<span class="c"># Don't set PRISMA_DATABASE_URL - triggers adapter path</span>
</code></pre></div></div>

<hr />

<h3 id="-pitfall-3-installing-adapter-as-regular-dependency">❌ Pitfall 3: Installing Adapter as Regular Dependency</h3>

<p><strong>Wrong:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> @prisma/adapter-pg
</code></pre></div></div>

<p><strong>Correct:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> <span class="nt">--save-dev</span> @prisma/adapter-pg
</code></pre></div></div>

<hr />

<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>

<h3 id="can-i-use-this-with-sqlite-for-testing">Can I use this with SQLite for testing?</h3>

<p>Yes! Install <code class="language-plaintext highlighter-rouge">@prisma/adapter-better-sqlite3</code> and modify the adapter import:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span> <span class="nx">PrismaSqlite</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">@prisma/adapter-better-sqlite3</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">adapter</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PrismaSqlite</span><span class="p">({</span> <span class="na">url</span><span class="p">:</span> <span class="dl">'</span><span class="s1">file:./test.db</span><span class="dl">'</span> <span class="p">});</span>
</code></pre></div></div>

<h3 id="will-this-work-with-prisma-6">Will this work with Prisma 6?</h3>

<p>The adapter strategy works with Prisma 6, but the <code class="language-plaintext highlighter-rouge">prisma.config.ts</code> file is Prisma 7+ only. For Prisma 6, keep <code class="language-plaintext highlighter-rouge">url = env("DATABASE_URL")</code> in your <code class="language-plaintext highlighter-rouge">schema.prisma</code>.</p>

<h3 id="do-i-need-docker-for-testing">Do I need Docker for testing?</h3>

<p>No, but it’s recommended. You can use any PostgreSQL instance – Docker just makes it easier to manage isolated test databases.</p>

<h3 id="can-i-use-this-with-other-orms">Can I use this with other ORMs?</h3>

<p>This strategy is Prisma-specific, but the concept of conditional database connections applies to any ORM.</p>

<h3 id="what-about-cicd-pipelines">What about CI/CD pipelines?</h3>

<p>In CI, set only <code class="language-plaintext highlighter-rouge">DATABASE_URL</code> (pointing to a test database) and omit <code class="language-plaintext highlighter-rouge">PRISMA_DATABASE_URL</code>. The adapter path will be used automatically.</p>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>The adapter strategy provides a clean, maintainable solution for managing database connections across environments. By conditionally using Prisma Accelerate in production and direct connections for testing, you get:</p>

<p>✅ Fast, isolated local testing<br />
✅ Production-grade performance with Accelerate<br />
✅ Optimized bundle sizes<br />
✅ Simple environment management<br />
✅ Lower costs</p>

<h3 id="next-steps">Next Steps</h3>

<ol>
  <li><strong>Implement the adapter strategy</strong> in your Next.js project</li>
  <li><strong>Set up Docker</strong> for your test database</li>
  <li><strong>Configure Cypress</strong> with database helpers</li>
  <li><strong>Write comprehensive e2e tests</strong> using your isolated test environment</li>
</ol>

<p>Ready to optimize your testing workflow? Start by installing the adapter and setting up your environment variables today!</p>

<hr />

<h3 id="additional-resources">Additional Resources</h3>

<ul>
  <li>📚 <a href="https://www.prisma.io/docs/accelerate">Prisma Accelerate Documentation</a></li>
  <li>🔧 <a href="https://www.prisma.io/docs/orm/overview/databases/postgresql#using-the-node-postgres-driver">Prisma Database Adapters Guide</a></li>
  <li>🎓 <a href="https://www.prisma.io/docs/orm/reference/prisma-schema-reference#datasource">Prisma 7 Configuration Reference</a></li>
  <li>🐳 <a href="https://hub.docker.com/_/postgres">Docker PostgreSQL Setup Guide</a></li>
</ul>

<hr />

<p><strong>Did this guide help you?</strong> Share your testing setup in the comments below or reach out if you have questions!</p>

<p><em>Tags: Prisma, Next.js, Testing, E2E Testing, Cypress, Docker, PostgreSQL, TypeScript</em></p>]]></content><author><name></name></author><summary type="html"><![CDATA[How to Use Prisma Adapters for Testing While Keeping Accelerate for Production]]></summary></entry><entry><title type="html">Welcome</title><link href="https://rhymion.github.io/blog/2026/02/17/welcome/" rel="alternate" type="text/html" title="Welcome" /><published>2026-02-17T00:00:00+00:00</published><updated>2026-02-17T00:00:00+00:00</updated><id>https://rhymion.github.io/blog/2026/02/17/welcome</id><content type="html" xml:base="https://rhymion.github.io/blog/2026/02/17/welcome/"><![CDATA[<p>This is the first post on the Rhymion Labs blog.</p>

<p>I’ll share notes on building generative systems, structured reasoning, and scalable architectures.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[This is the first post on the Rhymion Labs blog.]]></summary></entry></feed>