DEV Community

BeanBean
BeanBean

Posted on • Originally published at nextfuture.io.vn

Next.js Middleware in 2026: Beyond Auth — Advanced Patterns Most Developers Miss

Originally published on NextFuture

Next.js Middleware in 2026: Beyond Auth — Advanced Patterns Most Developers Miss

Most Next.js tutorials cover middleware exactly once: "Check if the user is logged in. If not, redirect to /login." Then they move on.

That's like buying a Swiss Army knife and only ever using the bottle opener.

Next.js Middleware runs on the Edge — before your request even touches your application. It executes globally, in milliseconds, with zero cold starts. And most developers are leaving 80% of its power on the table.

This guide covers five advanced middleware patterns that will change how you think about request handling in your Next.js app.


What Middleware Actually Is (And Why It's Different)

Before the patterns, let's get the mental model right.

Middleware in Next.js runs in the Vercel Edge Runtime (or equivalent), not in Node.js. This means:

  • No fs module
  • No native Node.js modules
  • Sub-millisecond execution expected
  • Runs before server-side rendering, API routes, and static files

It's not a general-purpose function — it's a routing layer with superpowers.

// middleware.ts — runs before every matching request
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  // You can read headers, cookies, geo info, URL
  // You can rewrite, redirect, or add headers
  // You CANNOT call databases, use heavy libraries, or run slow code
  return NextResponse.next()
}

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

With that constraint in mind, here's where the real power lies.


Pattern 1: A/B Testing Without a Third-Party Service

Most A/B testing tools (Optimizely, LaunchDarkly) add JavaScript to your page that flickers on load or slows First Contentful Paint. There's a better way.

By assigning variants at the Edge — before the response is even built — you get flicker-free A/B tests that render the correct variant on the server.

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

const VARIANT_COOKIE = 'ab-variant'

export function middleware(request: NextRequest) {
  const url = request.nextUrl.clone()

  // Only A/B test the homepage
  if (url.pathname !== '/') {
    return NextResponse.next()
  }

  const existingVariant = request.cookies.get(VARIANT_COOKIE)?.value
  const variant = existingVariant ?? (Math.random()  = {
  'new-dashboard': { percentage: 0.1 },           // 10% of users
  'ai-sidebar': { percentage: 1.0 },              // 100%
  'beta-api': { percentage: 0, allowlist: ['@acme.com', '@beta-tester.com'] },
}

export function isEnabled(flag: FeatureFlag, userId?: string, email?: string): boolean {
  const config = FLAG_CONFIG[flag]

  if (email && config.allowlist?.some(domain => email.endsWith(domain))) {
    return true
  }

  // Use userId for stable bucketing (same user always gets same result)
  if (userId) {
    const hash = userId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
    return (hash % 100) / 100 

# Welcome, {userId}


Role: {role}


  )
}
Enter fullscreen mode Exit fullscreen mode

This pattern eliminates redundant auth checks across RSCs and gives you a single place to control what context flows into your app. When you need to add a new piece of user context globally, you change one file — not twenty.


Pattern 4: Geo-Based Routing and Personalization

Next.js on Vercel exposes geo information directly on the request object. This is incredibly useful for compliance (GDPR banners), language routing, or serving region-specific content — all without JavaScript on the client.

// middleware.ts — geo-based routing
import { NextRequest, NextResponse } from 'next/server'

const EU_COUNTRIES = ['DE', 'FR', 'IT', 'ES', 'NL', 'PL', 'SE', 'BE', 'AT', 'DK']

export function middleware(request: NextRequest) {
  const country = request.geo?.country ?? 'US'
  const { pathname } = request.nextUrl

  // Redirect EU users to GDPR-compliant pricing page
  if (pathname === '/pricing' && EU_COUNTRIES.includes(country)) {
    return NextResponse.rewrite(new URL('/pricing/eu', request.url))
  }

  // Auto-redirect to localized homepage
  if (pathname === '/' && country === 'DE') {
    return NextResponse.redirect(new URL('/de/', request.url))
  }

  // Add country header for all routes so RSCs can use it
  const response = NextResponse.next()
  response.headers.set('x-country', country)
  return response
}
Enter fullscreen mode Exit fullscreen mode

Important caveat: request.geo is Vercel-specific. On other platforms, you'll need to parse a CF-IPCountry header (Cloudflare) or use a lightweight IP geolocation library. The pattern still works — just the data source changes.

Speaking of non-Vercel deployments: if you're running Next.js on your own infrastructure, Railway is an excellent alternative. It supports custom middleware patterns, has fast deploys, and doesn't lock you into a proprietary Edge Runtime.


Pattern 5: Edge Rate Limiting Without Extra Infrastructure

Classic rate limiting requires Redis, a background job, and extra infrastructure. For many use cases — especially protecting auth endpoints from brute force — you can do it at the Edge with nothing but cookies.

// middleware.ts — cookie-based rate limiting for auth endpoints
import { NextRequest, NextResponse } from 'next/server'

const WINDOW_MS = 15 * 60 * 1000  // 15 minutes
const MAX_ATTEMPTS = 5

function isRateLimited(request: NextRequest): boolean {
  const attempts = parseInt(request.cookies.get('auth-attempts')?.value ?? '0', 10)
  const lastAttemptStr = request.cookies.get('auth-last-attempt')?.value
  const lastAttempt = lastAttemptStr ? parseInt(lastAttemptStr, 10) : 0

  // Reset if window has passed
  if (Date.now() - lastAttempt > WINDOW_MS) return false

  return attempts >= MAX_ATTEMPTS
}

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

  if (pathname === '/api/auth/login' && request.method === 'POST') {
    if (isRateLimited(request)) {
      return new NextResponse(
        JSON.stringify({ error: 'Too many attempts. Try again in 15 minutes.' }),
        {
          status: 429,
          headers: {
            'Content-Type': 'application/json',
            'Retry-After': '900',
          },
        }
      )
    }
  }

  return NextResponse.next()
}
Enter fullscreen mode Exit fullscreen mode

For production-grade rate limiting with real persistence and distributed state, @upstash/ratelimit is fully Edge-compatible and uses Redis under the hood. The cookie approach above is good enough for simple protection without any dependencies.


Putting It All Together

Here's a production-ready middleware that combines multiple patterns without becoming a tangled mess:

// middleware.ts — production-ready combined example
import { NextRequest, NextResponse } from 'next/server'

const EU_COUNTRIES = new Set(['DE', 'FR', 'IT', 'ES', 'NL', 'PL', 'SE', 'BE', 'AT', 'DK'])

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

  // 1. Skip static assets early
  if (pathname.startsWith('/_next') || pathname.includes('.')) {
    return NextResponse.next()
  }

  const response = NextResponse.next()

  // 2. Geo enrichment
  const country = request.geo?.country ?? 'US'
  response.headers.set('x-country', country)
  response.headers.set('x-is-eu', EU_COUNTRIES.has(country) ? '1' : '0')

  // 3. A/B testing on homepage
  if (pathname === '/') {
    const existing = request.cookies.get('ab-variant')?.value
    const variant = existing ?? (Math.random() < 0.5 ? 'a' : 'b')
    if (!existing) {
      response.cookies.set('ab-variant', variant, { maxAge: 86400 * 30 })
    }
    response.headers.set('x-ab-variant', variant)
  }

  // 4. Auth context forwarding
  const token = request.cookies.get('auth-token')?.value
  if (token) {
    try {
      const claims = JSON.parse(atob(token.split('.')[1]))
      response.headers.set('x-user-id', claims.sub ?? '')
      response.headers.set('x-user-role', claims.role ?? 'guest')
    } catch { /* ignore malformed tokens */ }
  }

  return response
}

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

Clean, composable, and fast. Each concern is isolated and easy to remove or extend.


The Golden Rule of Middleware

Here's the thing that trips up most developers: middleware must be fast and stay Edge-compatible.

You cannot use:

  • Heavy libraries (lodash, moment, full auth SDKs like next-auth)
  • Node.js-only modules (fs, path, crypto from Node)
  • Slow database queries (Prisma, pg, Mongoose)
  • eval() or dynamic require()

You can use:

  • Lightweight pure-JS utilities
  • Web-standard APIs (fetch, crypto.subtle, atob, URL)
  • Edge-compatible SDKs (Upstash, Vercel KV, Cloudflare KV)
  • Any package marked "Edge Runtime compatible"

If your middleware is importing a full ORM or doing complex business logic, that logic belongs in an API route, a Server Action, or an RSC — not in the routing layer. Middleware is for routing decisions and lightweight enrichment.


Final Thoughts

Middleware is one of the most underutilized features in the Next.js ecosystem. While most developers treat it as a one-trick pony for auth redirects, it's actually a powerful layer for:

  • Routing logic (A/B tests, feature flags, geo targeting)
  • Context injection (headers for RSCs — eliminate redundant fetches)
  • Security (rate limiting, CSP headers, bot detection)
  • Personalization (geo, locale, user tier — zero JavaScript)

The next time you find yourself writing the same context-fetching logic in five different Server Components, ask yourself: could middleware handle this once, upstream, for all of them?

The answer is usually yes.

Start with one pattern. Get comfortable with the Edge constraints. Then slowly move more routing decisions upstream — closer to the user, faster, and without any JavaScript payload on the client.

That's what middleware is actually for.


This article was originally published on NextFuture. Follow us for more frontend & AI engineering content.

Top comments (0)