<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Sayan Khan</title>
    <description>The latest articles on Forem by Sayan Khan (@sayankhan313).</description>
    <link>https://forem.com/sayankhan313</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3710099%2F601899fe-f251-4cd5-af98-af62e197c4c9.png</url>
      <title>Forem: Sayan Khan</title>
      <link>https://forem.com/sayankhan313</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/sayankhan313"/>
    <language>en</language>
    <item>
      <title>Edge Runtime vs Node.js in Next.js: Lessons from Role-Based Auth Middleware</title>
      <dc:creator>Sayan Khan</dc:creator>
      <pubDate>Wed, 14 Jan 2026 15:18:19 +0000</pubDate>
      <link>https://forem.com/sayankhan313/edge-runtime-vs-nodejs-in-nextjs-lessons-from-role-based-auth-middleware-1nph</link>
      <guid>https://forem.com/sayankhan313/edge-runtime-vs-nodejs-in-nextjs-lessons-from-role-based-auth-middleware-1nph</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;While building a &lt;strong&gt;Next.js application&lt;/strong&gt;, I had to implement role-based access control so users could only access routes meant for their role. I was already using &lt;strong&gt;JWT authentication&lt;/strong&gt; stored in cookies, and most of the &lt;strong&gt;authentication logic&lt;/strong&gt; worked as expected.&lt;/p&gt;

&lt;p&gt;However, when I introduced &lt;strong&gt;Next.js middleware&lt;/strong&gt; to enforce these checks earlier in the request lifecycle, I noticed that some of my assumptions—based on how backend logic usually works—no longer held true.&lt;/p&gt;

&lt;p&gt;This pushed me to understand how &lt;strong&gt;Next.js Edge runtime&lt;/strong&gt; behaves differently from the normal Node.js runtime, and why that difference matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  How My Application Initially Handled Authentication
&lt;/h2&gt;

&lt;p&gt;In my application, authentication was based on JWTs stored in cookies. This allowed the app to remember the user without making repeated API calls.&lt;/p&gt;

&lt;p&gt;This approach worked well in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API routes running on the &lt;strong&gt;Node.js runtime&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Server-side logic where user data could be extracted easily&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because of this, I initially expected the same logic to behave the same way everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Middleware Changed the Picture
&lt;/h2&gt;

&lt;p&gt;The challenge appeared when I moved role-based checks into &lt;strong&gt;middleware.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The goal was simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check the user’s role before routing&lt;/li&gt;
&lt;li&gt;Prevent unauthorized users from reaching pages&lt;/li&gt;
&lt;li&gt;Avoid waiting for API calls to fail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even though the same JWT logic worked in backend routes, middleware behaved differently. That’s when I realized this wasn’t just an authentication issue—it was a &lt;strong&gt;runtime difference.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Node.js Runtime vs Edge Runtime (What I Learned)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F73guvzz7e7g6q7ddf66f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F73guvzz7e7g6q7ddf66f.png" alt="Next.js Edge runtime vs Node.js runtime behavior" width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Based on this experience, here’s how I now think about the two runtimes in Next.js.&lt;/p&gt;

&lt;h3&gt;
  
  
  Node.js Runtime
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Long-running execution model&lt;/li&gt;
&lt;li&gt;Supports full Node.js APIs&lt;/li&gt;
&lt;li&gt;Suitable for database access and sessions&lt;/li&gt;
&lt;li&gt;Works well for business logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This runtime behaves similarly to traditional Express-style servers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edge Runtime
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Stateless and request-based&lt;/li&gt;
&lt;li&gt;No persistent memory&lt;/li&gt;
&lt;li&gt;No Node.js APIs&lt;/li&gt;
&lt;li&gt;Designed for lightweight, fast checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This difference explains why logic that works in backend routes may behave differently in middleware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Edge Middleware Made Sense for Role-Based Checks
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6kggekkb3gnj3anq4ni0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6kggekkb3gnj3anq4ni0.png" alt="Next.js middleware request flow" width="800" height="581"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Middleware runs &lt;strong&gt;before pages and API routes&lt;/strong&gt;, which makes it ideal for enforcing access rules early.&lt;/p&gt;

&lt;p&gt;In my case, middleware was used to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read JWTs from cookies&lt;/li&gt;
&lt;li&gt;Verify the token using Edge-compatible logic&lt;/li&gt;
&lt;li&gt;Extract the user role&lt;/li&gt;
&lt;li&gt;Allow or block access before routing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ensured role checks happened before any page rendering or API execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Middleware Code
&lt;/h2&gt;

&lt;p&gt;Here’s a simplified version of the middleware I worked with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;verifyEdgeToken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/edgeAuth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyEdgeToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/ec&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/voter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VOTER&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key point is that this logic is intentionally minimal, because Edge middleware is meant for early decisions—not business logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Confused Me About Edge Runtime (At First)
&lt;/h2&gt;

&lt;p&gt;Initially, Edge runtime felt strange:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There was no persistent server&lt;/li&gt;
&lt;li&gt;Each request felt isolated&lt;/li&gt;
&lt;li&gt;Nothing stayed in memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Coming from a Node.js mindset, this felt like something was broken.&lt;/p&gt;

&lt;p&gt;Eventually, I understood that this is expected behavior in Edge environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Finally Understood
&lt;/h2&gt;

&lt;p&gt;Edge runtime is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stateless&lt;/li&gt;
&lt;li&gt;Request-scoped&lt;/li&gt;
&lt;li&gt;Designed for scalability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JWT-based authentication works well&lt;/li&gt;
&lt;li&gt;Session-based or in-memory approaches do not&lt;/li&gt;
&lt;li&gt;Middleware logic must remain lightweight&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once I accepted these constraints, middleware behavior made sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Edge Runtime Fits (and Where It Doesn’t)
&lt;/h2&gt;

&lt;p&gt;Good use cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication and authorization&lt;/li&gt;
&lt;li&gt;Route protection&lt;/li&gt;
&lt;li&gt;Redirects&lt;/li&gt;
&lt;li&gt;Lightweight request validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Poor use cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database access&lt;/li&gt;
&lt;li&gt;Heavy computation&lt;/li&gt;
&lt;li&gt;Business logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Understanding this boundary helped me design cleaner Next.js architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This experience taught me that runtime choice in Next.js is an architectural decision, not just a performance detail.&lt;/p&gt;

&lt;p&gt;Using middleware for role-based checks helped me understand how &lt;strong&gt;Edge runtime and Node.js runtime complement each other&lt;/strong&gt;—and why using each in the right place matters.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>authentication</category>
      <category>edge</category>
    </item>
  </channel>
</rss>
