IAnswerable

Protect Routes with Next.js Middleware

12 Jan 2026

Next.js middleware runs on the edge, before your page or API route ever executes. That makes it a good place to block unauthenticated requests early — and a bad place to do anything that needs a real database connection.

What middleware can and can’t do

Middleware runs in the Edge Runtime, a restricted environment that isn’t Node.js. No fs, no most native npm packages, no direct Postgres/MySQL driver connections (most of those depend on TCP sockets, which the edge runtime doesn’t expose the same way). What it does have: fetch, cookies, headers, and enough JS to make a routing decision fast.

That means middleware is good for checking whether a session token exists and looks valid and bad for checking whether that session is still valid against your database. Keep the expensive check in the actual route handler.

Here’s the decision middleware makes on every request, and where the real check has to happen instead:

flowchart TD
    A[Request hits middleware] --> B{Public path?}
    B -->|Yes| E[Allow through]
    B -->|No| C{Session cookie present?}
    C -->|No| D[Redirect to login]
    C -->|Yes, optionally jose-verified| F[Allow to route handler]
    F --> G["DB check: revoked? banned?\n(route handler only)"]
  

A real auth-gate middleware

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const PUBLIC_PATHS = ['/login', '/signup', '/api/auth']

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  const isPublic = PUBLIC_PATHS.some((p) => pathname.startsWith(p))
  const token = request.cookies.get('session')?.value

  if (!isPublic && !token) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('from', pathname)
    return NextResponse.redirect(loginUrl)
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

This checks for the presence of a session cookie, not its validity. That’s deliberate — it’s fast, and it stops the obvious case (no cookie at all) before the request reaches a page component.

The mistake: verifying the token in middleware

If your session token is a JWT and you’re tempted to verify its signature and expiry right in middleware, you can — jose works in the Edge Runtime, unlike most JWT libraries built on Node’s crypto. But don’t go further and try to check the session against a database or revocation list here. That requires a real DB client (a Node.js runtime, not edge), and pushing that into middleware either fails outright or forces you onto runtime: 'nodejs' middleware, which adds latency to every single request, including ones that don’t need auth at all.

import { jwtVerify } from 'jose'

const secret = new TextEncoder().encode(process.env.JWT_SECRET)

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value
  if (!token) return redirectToLogin(request)

  try {
    await jwtVerify(token, secret)
    return NextResponse.next()
  } catch {
    return redirectToLogin(request)
  }
}

This confirms the token hasn’t been tampered with and hasn’t expired — not that the user hasn’t been banned or logged out server-side five minutes ago. Do that check in the route handler or a server component, where a normal database client works.

What middleware can’t protect

Middleware only runs for requests that hit your Next.js server. It does nothing for:

Never treat middleware as your only authorization layer. Route handlers and server actions should re-check auth themselves — middleware is a fast first filter, not the final word.

The takeaway

Use middleware to reject obviously unauthenticated requests fast, using cookie presence or a signature check with jose. Push database-backed validation (session revocation, role checks) into route handlers where a real Node.js runtime and DB client are available. Never rely on middleware as the sole gate.