Introduction
While building a Next.js application, I had to implement role-based access control so users could only access routes meant for their role. I was already using JWT authentication stored in cookies, and most of the authentication logic worked as expected.
However, when I introduced Next.js middleware 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.
This pushed me to understand how Next.js Edge runtime behaves differently from the normal Node.js runtime, and why that difference matters.
How My Application Initially Handled Authentication
In my application, authentication was based on JWTs stored in cookies. This allowed the app to remember the user without making repeated API calls.
This approach worked well in:
- API routes running on the Node.js runtime
- Server-side logic where user data could be extracted easily
Because of this, I initially expected the same logic to behave the same way everywhere.
Where Middleware Changed the Picture
The challenge appeared when I moved role-based checks into middleware.
The goal was simple:
- Check the user’s role before routing
- Prevent unauthorized users from reaching pages
- Avoid waiting for API calls to fail
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 runtime difference.
Node.js Runtime vs Edge Runtime (What I Learned)
Based on this experience, here’s how I now think about the two runtimes in Next.js.
Node.js Runtime
- Long-running execution model
- Supports full Node.js APIs
- Suitable for database access and sessions
- Works well for business logic
This runtime behaves similarly to traditional Express-style servers.
Edge Runtime
- Stateless and request-based
- No persistent memory
- No Node.js APIs
- Designed for lightweight, fast checks
This difference explains why logic that works in backend routes may behave differently in middleware.
Why Edge Middleware Made Sense for Role-Based Checks
Middleware runs before pages and API routes, which makes it ideal for enforcing access rules early.
In my case, middleware was used to:
- Read JWTs from cookies
- Verify the token using Edge-compatible logic
- Extract the user role
- Allow or block access before routing
This ensured role checks happened before any page rendering or API execution.
The Middleware Code
Here’s a simplified version of the middleware I worked with:
import { NextRequest, NextResponse } from "next/server";
import { verifyEdgeToken } from "@/lib/edgeAuth";
export async function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
const token = req.cookies.get("token")?.value;
if (!token) {
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
const payload = await verifyEdgeToken(token);
if (pathname.startsWith("/ec") && payload.role !== "EC") {
return NextResponse.json({ message: "Forbidden" }, { status: 403 });
}
if (pathname.startsWith("/voter") && payload.role !== "VOTER") {
return NextResponse.json({ message: "Forbidden" }, { status: 403 });
}
return NextResponse.next();
}
The key point is that this logic is intentionally minimal, because Edge middleware is meant for early decisions—not business logic.
What Confused Me About Edge Runtime (At First)
Initially, Edge runtime felt strange:
- There was no persistent server
- Each request felt isolated
- Nothing stayed in memory
Coming from a Node.js mindset, this felt like something was broken.
Eventually, I understood that this is expected behavior in Edge environments.
What I Finally Understood
Edge runtime is:
- Stateless
- Request-scoped
- Designed for scalability
This is why:
- JWT-based authentication works well
- Session-based or in-memory approaches do not
- Middleware logic must remain lightweight
Once I accepted these constraints, middleware behavior made sense.
Where Edge Runtime Fits (and Where It Doesn’t)
Good use cases:
- Authentication and authorization
- Route protection
- Redirects
- Lightweight request validation
Poor use cases:
- Database access
- Heavy computation
- Business logic
Understanding this boundary helped me design cleaner Next.js architecture.
Final Thoughts
This experience taught me that runtime choice in Next.js is an architectural decision, not just a performance detail.
Using middleware for role-based checks helped me understand how Edge runtime and Node.js runtime complement each other—and why using each in the right place matters.


Top comments (0)