Back to Blog

Next.js Middleware Patterns: Auth, Redirects, A/B Testing, and Geo Routing at the Edge

Master Next.js Edge Middleware: implement authentication guards without full server roundtrips, configure redirect rules, run A/B tests at the edge with cookie-based assignment, and geo-route users to regional content — all with sub-millisecond latency.

Viprasol Tech Team
October 26, 2026
13 min read

Next.js Middleware runs at the Edge — before the request reaches your server, on Vercel's CDN nodes or your own edge infrastructure. This makes it ideal for fast operations that must happen on every request: auth checks, redirects, bot filtering, A/B test assignment, and geo-based routing.

Edge Middleware runs in a restricted runtime (no Node.js fs, child_process, or most npm packages) but has access to request headers, cookies, URL, and geolocation data from the CDN.


Middleware Structure

// middleware.ts — at the root of your project (not src/)

import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest): NextResponse | Response {
  const { pathname } = request.nextUrl;

  // Auth check
  const authResponse = checkAuth(request);
  if (authResponse) return authResponse;

  // A/B test assignment
  const abResponse = assignABTest(request);
  if (abResponse) return abResponse;

  // Geo routing
  return handleGeoRouting(request);
}

// Matcher: which paths middleware runs on
// Be specific — running on every request adds latency
export const config = {
  matcher: [
    // Run on all paths except static files and API routes that handle their own auth
    "/((?!_next/static|_next/image|favicon.ico|public/).*)",
  ],
};

Authentication Middleware

// src/middleware/auth.ts

import { NextRequest, NextResponse } from "next/server";
import { jwtVerify } from "jose";  // jose works in Edge Runtime; jsonwebtoken does not

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

// Routes that don't require authentication
const PUBLIC_PATHS = [
  "/",
  "/login",
  "/signup",
  "/forgot-password",
  "/api/auth",    // Auth API routes handle their own logic
  "/blog",        // Public blog
];

const isPublicPath = (pathname: string): boolean => {
  return PUBLIC_PATHS.some(
    (path) => pathname === path || pathname.startsWith(`${path}/`)
  );
};

export async function checkAuth(
  request: NextRequest
): Promise<NextResponse | null> {
  const { pathname } = request.nextUrl;

  if (isPublicPath(pathname)) return null; // No auth required

  const token =
    request.cookies.get("access_token")?.value ||
    request.headers.get("authorization")?.replace("Bearer ", "");

  if (!token) {
    // Redirect to login, preserving the intended destination
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("returnTo", pathname);
    return NextResponse.redirect(loginUrl);
  }

  try {
    const { payload } = await jwtVerify(token, JWT_SECRET);

    // Forward user context to route handlers via request headers
    // (you can't pass data directly — use headers instead)
    const requestHeaders = new Headers(request.headers);
    requestHeaders.set("x-user-id", payload.sub as string);
    requestHeaders.set("x-tenant-id", payload.tenantId as string);
    requestHeaders.set("x-user-roles", JSON.stringify(payload.roles));

    return NextResponse.next({
      request: { headers: requestHeaders },
    });
  } catch {
    // Token invalid or expired — clear cookie and redirect to login
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("returnTo", pathname);
    loginUrl.searchParams.set("reason", "session_expired");

    const response = NextResponse.redirect(loginUrl);
    response.cookies.delete("access_token");
    return response;
  }
}

🌐 Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript — production-grade stack
  • Fixed-price contracts — no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Redirect Rules

// src/middleware/redirects.ts
// Handle redirects at the edge — faster than server-side redirects

interface RedirectRule {
  source: string | RegExp;
  destination: string;
  permanent: boolean;
}

const REDIRECT_RULES: RedirectRule[] = [
  // Legacy URL cleanup (match GSC 404s)
  { source: "/blog/old-slug", destination: "/blog/new-slug", permanent: true },
  { source: "/services/devops", destination: "/services/cloud-solutions", permanent: true },

  // Trailing slash normalization
  // (or configure this in next.config.ts with trailingSlash)

  // Campaign redirects
  { source: "/go/product-hunt", destination: "/?utm_source=producthunt&utm_medium=launch", permanent: false },
];

export function handleRedirects(request: NextRequest): NextResponse | null {
  const { pathname } = request.nextUrl;

  for (const rule of REDIRECT_RULES) {
    const matches =
      typeof rule.source === "string"
        ? pathname === rule.source
        : rule.source.test(pathname);

    if (matches) {
      const destination = new URL(rule.destination, request.url);
      return NextResponse.redirect(destination, {
        status: rule.permanent ? 308 : 307,
      });
    }
  }

  return null;
}

A/B Testing at the Edge

// src/middleware/ab-testing.ts
// Assign users to test variants at the edge — no flicker, no layout shift

interface ABTest {
  id: string;
  variants: string[];
  weights: number[];  // Must sum to 1
  // Paths this test applies to
  paths: string[];
}

const ACTIVE_TESTS: ABTest[] = [
  {
    id: "pricing-page-cta",
    variants: ["control", "variant-a", "variant-b"],
    weights: [0.34, 0.33, 0.33],
    paths: ["/pricing"],
  },
  {
    id: "signup-flow",
    variants: ["control", "simplified"],
    weights: [0.5, 0.5],
    paths: ["/signup"],
  },
];

function assignVariant(test: ABTest): string {
  const rand = Math.random();
  let cumulative = 0;

  for (let i = 0; i < test.variants.length; i++) {
    cumulative += test.weights[i];
    if (rand < cumulative) return test.variants[i];
  }

  return test.variants[0]; // Fallback
}

export function assignABTest(request: NextRequest): NextResponse | null {
  const { pathname } = request.nextUrl;
  let response: NextResponse | null = null;

  for (const test of ACTIVE_TESTS) {
    if (!test.paths.some((p) => pathname.startsWith(p))) continue;

    const cookieName = `ab_${test.id}`;
    const existingVariant = request.cookies.get(cookieName)?.value;

    if (existingVariant && test.variants.includes(existingVariant)) {
      // Already assigned — forward variant to page via header
      if (!response) {
        response = NextResponse.next();
      }
      response.headers.set(`x-ab-${test.id}`, existingVariant);
      continue;
    }

    // New visitor — assign variant
    const variant = assignVariant(test);

    if (!response) {
      response = NextResponse.next();
    }

    // Set cookie (persists assignment across sessions)
    response.cookies.set(cookieName, variant, {
      maxAge: 30 * 24 * 60 * 60, // 30 days
      httpOnly: false,             // Readable by analytics scripts
      sameSite: "lax",
    });

    response.headers.set(`x-ab-${test.id}`, variant);
  }

  return response;
}
// Reading the A/B variant in a page
// src/app/pricing/page.tsx

import { headers } from "next/headers";

export default async function PricingPage() {
  const headersList = headers();
  const variant = headersList.get("x-ab-pricing-page-cta") ?? "control";

  return (
    <main>
      {variant === "variant-a" ? (
        <button className="bg-green-600 text-white px-8 py-4 text-lg rounded-xl">
          Start Building Free →
        </button>
      ) : variant === "variant-b" ? (
        <button className="bg-blue-600 text-white px-8 py-4 text-lg rounded-xl">
          Get Started in 2 Minutes
        </button>
      ) : (
        <button className="bg-blue-600 text-white px-6 py-3 rounded-lg">
          Start Free Trial
        </button>
      )}
    </main>
  );
}

🚀 Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4–8 weeks, full platforms in 3–5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Geo Routing

// src/middleware/geo-routing.ts
// Route users to region-specific content or servers

export function handleGeoRouting(request: NextRequest): NextResponse {
  const country = request.geo?.country ?? "US";
  const { pathname } = request.nextUrl;

  // Route to regional subpages for compliance (GDPR notice for EU)
  if (pathname === "/" && isEUCountry(country)) {
    // Rewrite (not redirect) — URL stays as "/" but serves EU content
    const url = request.nextUrl.clone();
    url.pathname = "/eu";
    return NextResponse.rewrite(url);
  }

  // Route to language-appropriate content
  const preferredLocale = getPreferredLocale(request, country);
  if (preferredLocale !== "en" && !pathname.startsWith(`/${preferredLocale}`)) {
    const url = request.nextUrl.clone();
    url.pathname = `/${preferredLocale}${pathname}`;
    return NextResponse.redirect(url, { status: 307 });
  }

  // Set country header for server components
  const response = NextResponse.next();
  response.headers.set("x-user-country", country);
  response.headers.set("x-user-region", request.geo?.region ?? "");
  return response;
}

const EU_COUNTRIES = new Set([
  "AT", "BE", "BG", "CY", "CZ", "DE", "DK", "EE", "ES", "FI",
  "FR", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT",
  "NL", "PL", "PT", "RO", "SE", "SI", "SK",
]);

function isEUCountry(country: string): boolean {
  return EU_COUNTRIES.has(country);
}

function getPreferredLocale(request: NextRequest, country: string): string {
  // Check explicit cookie preference first
  const cookieLocale = request.cookies.get("locale")?.value;
  if (cookieLocale) return cookieLocale;

  // Infer from country
  const countryToLocale: Record<string, string> = {
    DE: "de", FR: "fr", ES: "es", JP: "ja", BR: "pt",
  };

  return countryToLocale[country] ?? "en";
}

Bot Detection and Rate Limiting

// src/middleware/bot-filter.ts
// Simple bot detection at the edge — before traffic hits your server

const BOT_USER_AGENTS = [
  /Googlebot/i, /bingbot/i, /Slurp/i, /DuckDuckBot/i,
  /Baiduspider/i, /YandexBot/i, /Sogou/i,
  // Add scrapers you want to block
  /python-requests/i, /Go-http-client/i,
];

const ALLOWED_CRAWLERS = new Set(["Googlebot", "bingbot"]);

export function filterBots(request: NextRequest): NextResponse | null {
  const ua = request.headers.get("user-agent") ?? "";

  // Allow known search engine crawlers
  if (ALLOWED_CRAWLERS.has(ua.split("/")[0])) return null;

  // Block scraper patterns on sensitive endpoints
  const sensitivePattern = /^\/api\/(?!auth)/;
  if (sensitivePattern.test(request.nextUrl.pathname)) {
    for (const pattern of BOT_USER_AGENTS) {
      if (pattern.test(ua)) {
        return new NextResponse("Too Many Requests", { status: 429 });
      }
    }
  }

  return null;
}

See Also


Working With Viprasol

Edge Middleware enables authentication, personalization, A/B testing, and routing decisions with minimal latency overhead. We implement middleware patterns for Next.js applications — from session-based auth guards to multi-region geo routing — with proper matcher configuration that avoids running middleware on every static asset request.

Next.js engineering → | Start a project →

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms — we build it all with Next.js and React.

Free consultation • No commitment • Response within 24 hours

Viprasol · Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.