DEV Community

Dugg
Dugg

Posted on

Middleware in Next.js - The Right Way.

Middleware in Next.js – The Right Way

When you start working with middleware in Next.js, it feels easy enough:
add a couple of redirects, protect some routes, done.

But here’s the problem: most developers (including past me) end up writing middleware like this:

export async function middleware(request: NextRequest, user: User | null) { const { pathname } = request.nextUrl; if (pathname.startsWith("/dashboard")) { if (!user) { return NextResponse.redirect(new URL("/login", request.url)); } } else if (pathname.startsWith("/onboarding")) { if (!user) { return NextResponse.redirect(new URL("/login", request.url)); } } if (pathname.startsWith("/login")) { if (user) { return NextResponse.redirect(new URL("/dashboard", request.url)); } } else if (pathname.startsWith("/signup")) { if (user) { return NextResponse.redirect(new URL("/dashboard", request.url)); } } return NextResponse.next(); } 
Enter fullscreen mode Exit fullscreen mode

Even though this works fine at a first glance. But there's a big problem in this approach.

As soon as you add more routes (e.g. /profile, /settings, /forgot-password, etc.)... your file turns into a giant mess of if/else.

And it will only become worse as your project scales.

Not fun, not scalable, and definitely - not clean.


Think in Rules, Not Conditions

Instead of hardcoding every redirect, we must only define conditions.

Think of a middleware as of a traffic cop.

The cop doesn't care who you are. It only checks where you’re going and whether you’re allowed to be there.

So let’s describe our rules in a structured way.


Step 1: Define Route Groups

We’ll split our routes into two simple groups:

const PROTECTED_ROUTES = ["/dashboard", "/onboarding"]; const AUTH_ROUTES = ["/login", "/signup"]; 
Enter fullscreen mode Exit fullscreen mode
  • Protected routes: only logged-in users can access them.
  • Auth routes: only logged-out users should see them.

Already, much cleaner than writing 20 if checks.


Step 2: Describe Rules

Now let’s create a type for rules:

interface RouteRule { routes: string[]; condition: (user: User | null) => boolean; redirect: string; } 
Enter fullscreen mode Exit fullscreen mode

Each rule says:

  • Which routes it applies to
  • The condition that triggers a redirect
  • Where to redirect to

Step 3: Write the Rules

Here’s what our two rules look like:

const routeRules: RouteRule[] = [ { routes: PROTECTED_ROUTES, condition: (user) => !user, // if no user, redirect redirect: "/login", }, { routes: AUTH_ROUTES, condition: (user) => !!user, // if logged in, redirect redirect: "/dashboard", }, ]; 
Enter fullscreen mode Exit fullscreen mode

That’s it. Add more groups? Just add more objects.
No need to touch the logic again.


Step 4: Apply Rules in Middleware

Now we loop through the rules:

export class Middleware { private routeRules = routeRules; async handle(request: NextRequest, user: User | null) { const { pathname } = request.nextUrl; for (const rule of this.routeRules) { const isMatch = rule.routes.some((r) => pathname.startsWith(r) ); if (isMatch && rule.condition(user)) { return NextResponse.redirect( new URL(rule.redirect, request.url) ); } } return NextResponse.next(); } } 
Enter fullscreen mode Exit fullscreen mode

No clutter. No chaos. Just clear logic.


Why This Is the “Right Way”

  1. Scalable – Add new routes in seconds
  2. Readable – No jungle of if/else
  3. Reusable – One central place for rules
  4. Extensible – Can be expanded for roles, permissions, etc.
  5. SOLID compliant.

Middleware must feel like a middleware, not a nightmare.


Final Thoughts

The trick is simple:
Stop hardcoding conditions. Start thinking in rules.

This pattern will save you headaches, especially if your app has authentication and a growing set of protected pages.

If you found this interesting, comment, and let's have a talk!

Top comments (0)