Next.js 16 Middleware in 2026: Edge Runtime, Authentication and Interview Questions

Master Next.js 16 Middleware with Edge Runtime patterns, authentication strategies, and common interview questions. Learn request interception, path matching, and production-ready auth flows.

Next.js 16 Middleware in 2026: Edge Runtime, Authentication and Interview Questions

Next.js 16 Middleware runs before every request hits the server, making it the first line of defense for authentication, localization, and request manipulation. Understanding middleware is essential for building production applications and a frequent topic in frontend interviews.

Middleware runs at the Edge

Next.js 16 Middleware executes in the Edge Runtime by default, meaning it runs in data centers close to users with sub-millisecond cold starts. This makes it ideal for authentication checks and redirects that need to happen before the page renders.

How Next.js 16 Middleware Works Under the Hood

Middleware in Next.js 16 intercepts requests before they reach the route handler or page component. The middleware file must be placed at the root of the project (next to app/ or pages/) and exports a default function that receives a NextRequest object.

The Edge Runtime constraint means middleware cannot use Node.js APIs like fs or Buffer. It runs on V8 isolates, similar to Cloudflare Workers, which enables global distribution but limits available APIs to Web Standards.

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

export function middleware(request: NextRequest) {
  // Access request headers, cookies, and URL
  const token = request.cookies.get('session')?.value
  const pathname = request.nextUrl.pathname

  // Log for debugging (visible in Vercel logs)
  console.log(`Middleware: ${request.method} ${pathname}`)

  // Continue to the route handler
  return NextResponse.next()
}

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

The matcher config uses a regex-like syntax to define which paths trigger the middleware. Without it, middleware runs on every request, including static assets.

Authentication Patterns with Next.js Middleware

Middleware excels at authentication because it runs before any page code executes. A user without a valid session never sees protected content, not even briefly. This differs from client-side auth checks that can flash protected content before redirecting.

The following pattern validates a session token and redirects unauthenticated users. It integrates with any auth provider that stores session data in cookies or headers.

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

const protectedPaths = ['/dashboard', '/settings', '/profile']
const authPaths = ['/login', '/signup']

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  const sessionToken = request.cookies.get('session')?.value

  // Check if path requires authentication
  const isProtectedPath = protectedPaths.some(path => 
    pathname.startsWith(path)
  )
  const isAuthPath = authPaths.some(path => 
    pathname.startsWith(path)
  )

  // Validate session token (call auth service)
  const isValidSession = sessionToken 
    ? await validateSession(sessionToken)
    : false

  // Redirect unauthenticated users to login
  if (isProtectedPath && !isValidSession) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('callbackUrl', pathname)
    return NextResponse.redirect(loginUrl)
  }

  // Redirect authenticated users away from auth pages
  if (isAuthPath && isValidSession) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }

  return NextResponse.next()
}

async function validateSession(token: string): Promise<boolean> {
  // Call auth API endpoint or decode JWT
  // Edge-compatible: use fetch, not Node.js crypto
  try {
    const response = await fetch('https://api.example.com/validate', {
      headers: { Authorization: `Bearer ${token}` },
    })
    return response.ok
  } catch {
    return false
  }
}

The callbackUrl parameter preserves the original destination, so users land on the page they initially requested after logging in.

Path Matching and the Matcher Config

The matcher configuration determines which requests trigger middleware. Getting this wrong leads to performance issues (middleware running on every static asset) or security holes (protected paths not triggering auth checks).

Next.js 16 supports three matcher syntaxes: string paths, path patterns with parameters, and regex-like patterns with negative lookaheads.

middleware.tstypescript
// Option 1: Simple string paths
export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
}

// Option 2: Exclude static assets with negative lookahead
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|public/).*)'],
}

// Option 3: Conditional matching with has/missing
export const config = {
  matcher: [
    {
      source: '/api/:path*',
      has: [{ type: 'header', key: 'Authorization' }],
    },
    {
      source: '/dashboard/:path*',
      missing: [{ type: 'cookie', key: 'session' }],
    },
  ],
}

The has and missing conditions in option 3 add logic before middleware even runs. This is more efficient than checking inside the middleware function.

Ready to ace your React / Next.js interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Request and Response Manipulation in Middleware

Beyond authentication, middleware can modify requests before they reach route handlers and responses before they reach the client. Common use cases include adding security headers, rewriting URLs for A/B testing, and injecting geolocation data.

The NextResponse class provides methods for each modification type. Rewrites change the destination without changing the browser URL, while redirects update both.

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

export function middleware(request: NextRequest) {
  const response = NextResponse.next()

  // Add security headers to all responses
  response.headers.set('X-Frame-Options', 'DENY')
  response.headers.set('X-Content-Type-Options', 'nosniff')
  response.headers.set(
    'Referrer-Policy', 
    'strict-origin-when-cross-origin'
  )

  // Inject geolocation into request headers for route handlers
  const geo = request.geo
  if (geo?.country) {
    response.headers.set('x-user-country', geo.country)
    response.headers.set('x-user-city', geo.city ?? 'unknown')
  }

  return response
}

Geolocation data (request.geo) is populated by Vercel's Edge Network. Self-hosted deployments need to configure this through the hosting provider or use a third-party IP geolocation service.

URL Rewriting for A/B Testing and Feature Flags

Middleware rewrites let you serve different pages based on cookies, headers, or random assignment without the client knowing. This pattern powers A/B testing frameworks and gradual feature rollouts.

The rewrite happens at the edge, so both variants have the same URL and benefit from edge caching when appropriate.

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

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl

  // Check for existing variant assignment
  let variant = request.cookies.get('ab-variant')?.value

  // Assign variant if not set (50/50 split)
  if (!variant && pathname === '/pricing') {
    variant = Math.random() < 0.5 ? 'control' : 'treatment'
    const response = NextResponse.rewrite(
      new URL(`/pricing/${variant}`, request.url)
    )
    response.cookies.set('ab-variant', variant, {
      maxAge: 60 * 60 * 24 * 30, // 30 days
      httpOnly: true,
    })
    return response
  }

  // Rewrite to assigned variant
  if (variant && pathname === '/pricing') {
    return NextResponse.rewrite(
      new URL(`/pricing/${variant}`, request.url)
    )
  }

  return NextResponse.next()
}

The cookie persists the variant assignment, ensuring users see the same version on return visits. The 30-day expiry balances experiment consistency with the ability to reassign users to new experiments.

Edge Runtime Limitations and Workarounds

The Edge Runtime trades Node.js compatibility for global distribution and fast cold starts. Middleware cannot import Node.js-specific modules, which affects libraries like bcrypt, jsonwebtoken (when using RS256), and database clients that rely on TCP sockets.

The Next.js Edge Runtime documentation lists supported APIs. Web Crypto, fetch, and most Web Platform APIs work. Heavy computation should move to API routes running in Node.js.

middleware.ts - JWT validation without jsonwebtokentypescript
import { jwtVerify } from 'jose' // Edge-compatible JWT library
import type { NextRequest } from 'next/server'

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

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value

  if (!token) {
    return redirectToLogin(request)
  }

  try {
    // jose library works in Edge Runtime
    const { payload } = await jwtVerify(token, JWT_SECRET)
    
    // Token is valid, check expiration
    if (payload.exp && payload.exp < Date.now() / 1000) {
      return redirectToLogin(request)
    }

    return NextResponse.next()
  } catch {
    // Invalid token signature or format
    return redirectToLogin(request)
  }
}

function redirectToLogin(request: NextRequest) {
  const url = new URL('/login', request.url)
  url.searchParams.set('callbackUrl', request.nextUrl.pathname)
  return NextResponse.redirect(url)
}

The jose library provides Edge-compatible JWT operations. It uses Web Crypto under the hood, avoiding the Node.js crypto module.

Common Next.js Middleware Interview Questions

Interviewers ask about middleware to assess understanding of request lifecycle, security patterns, and Edge computing constraints. These questions appear in senior frontend and fullstack roles working with Next.js.

Where does middleware execute in the request lifecycle?

Middleware runs after the request hits the Next.js server but before any route matching or page rendering. On Vercel, it executes at the Edge, meaning the closest data center to the user. This placement makes it ideal for authentication and redirects since invalid requests never reach the origin server.

What can middleware not do compared to API routes?

Middleware runs in the Edge Runtime, not Node.js. It cannot use Node.js built-in modules (fs, path, crypto with certain algorithms), TCP-based database clients, or npm packages that depend on Node.js APIs. Heavy computation should happen in API routes or Server Components.

How do you handle authentication in middleware without blocking?

The pattern involves checking for a session cookie, validating it against an auth endpoint or by verifying a JWT signature, and redirecting if invalid. To avoid blocking every request, use the matcher config to only run middleware on protected paths. Stateless JWT validation is faster than calling an external auth service on every request.

What happens if middleware throws an error?

An unhandled error in middleware returns a 500 response to the client. The page never renders. Wrapping middleware logic in try-catch and returning a fallback response (often NextResponse.next()) prevents complete request failures. Logging the error before continuing helps with debugging.

For deeper practice with these concepts, the Next.js Middleware and Auth interview questions module covers additional scenarios.

Debugging Middleware in Development and Production

Middleware bugs are tricky because the code runs at the edge, not in the browser DevTools. Development mode shows middleware logs in the terminal, but production requires proper logging infrastructure.

The following approach logs middleware execution with enough context to debug issues without exposing sensitive data.

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

export function middleware(request: NextRequest) {
  const start = Date.now()
  const requestId = crypto.randomUUID()

  // Log request start
  console.log(JSON.stringify({
    type: 'middleware_request',
    requestId,
    method: request.method,
    path: request.nextUrl.pathname,
    userAgent: request.headers.get('user-agent')?.slice(0, 100),
    country: request.geo?.country,
  }))

  // Process request
  const response = processRequest(request)

  // Log completion
  console.log(JSON.stringify({
    type: 'middleware_response',
    requestId,
    duration: Date.now() - start,
    status: response.status,
  }))

  // Pass request ID to route handlers
  response.headers.set('x-request-id', requestId)
  return response
}

function processRequest(request: NextRequest): NextResponse {
  // Middleware logic here
  return NextResponse.next()
}

On Vercel, these logs appear in the Functions tab. Self-hosted deployments should send logs to a service like Datadog or use structured logging that the hosting platform can ingest.

Middleware Performance Best Practices

Middleware runs on every matched request. A slow middleware function adds latency to every page load. The goal is to complete middleware execution in under 5ms for most requests.

  • Minimize async operations: Each await adds latency. Cache validation results when possible.
  • Use matcher config: Exclude static assets and public paths that do not need middleware logic.
  • Avoid external calls on every request: Validate JWTs locally instead of calling an auth service. Cache results in cookies when appropriate.
  • Keep the bundle small: Middleware has its own bundle size limit (1MB on Vercel). Import only what is needed.

The React Next.js technology page at /technologies/react-next covers related concepts like Server Components and data fetching patterns that complement middleware knowledge.

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Next.js 16 Middleware Checklist for Production

  • Place middleware at the project root as middleware.ts, not inside app/ or pages/
  • Configure the matcher to exclude _next/static, _next/image, and public assets from middleware execution
  • Use Edge-compatible libraries for JWT validation, such as jose instead of jsonwebtoken
  • Store callback URLs in query parameters when redirecting to login, so users return to their intended destination
  • Add security headers in middleware for consistent application across all routes
  • Log middleware execution with request IDs to trace requests through the system
  • Keep external API calls to a minimum by validating tokens locally and caching user data in signed cookies
Daily challenge

Can you spot the bug in React / Next.js?

One real snippet, one hidden bug, one attempt a day. No account needed to try.

Anthony Fillion-Maillet

Written by

Anthony Fillion-Maillet

Founder of SharpSkill

Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.

Updated on September 20, 2026

Share

Related articles