DEV Community

Ahmed Mahmoud
Ahmed Mahmoud

Posted on • Originally published at devya.dev

Next.js Middleware in 2026: Auth Guards, A/B Tests, and What Belongs at the Edge

Headline: Next.js Middleware (middleware.ts at the project root) runs before every matched request — before cache, before rendering, before the route. That position makes it right for auth redirects, A/B cookie bucketing, and locale detection. Wrong for database queries and heavy imports. In 2026, Middleware on Vercel runs on Fluid Compute (standard Node.js), so the constraint is latency budget, not API availability.

Key takeaways

  • Middleware runs before every matched request — before cache, rendering, or route handler — the right layer for auth, locale, and A/B bucketing.
  • Middleware can read requests, set cookies, redirect, rewrite, or return early — without the route running. DB queries and large packages add latency to every request.
  • On Vercel in 2026, Middleware runs on Fluid Compute (standard Node.js). The constraint is latency: every added millisecond is paid on every matched request.
  • Use matcher to scope Middleware to only the routes that need it; without it Middleware runs on every static asset request.
  • Auth in Middleware = verifying a self-contained JWT without a DB call. Full session validation belongs in the route.

I spent a long time only using Middleware for locale redirects. After shipping auth-protected routes and an A/B test, the full shape became clear.

What is Next.js Middleware and where does it run?

Middleware is exported from middleware.ts at the project root. It intercepts matched requests before route resolution, cache lookup, and Server Component execution. Returns one of four types: pass through (NextResponse.next()), redirect, rewrite (serve different content while keeping original URL in address bar), or a direct response.

export function middleware(request: NextRequest) {
  return NextResponse.next();
}
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
Enter fullscreen mode Exit fullscreen mode

Without matcher, Middleware runs on every request including static files. On Vercel in 2026, Middleware runs on Fluid Compute — standard Node.js. Aim for under 10 ms.

How do I write an auth guard?

Read a JWT from a cookie, verify locally with jose (no DB call), redirect to /login if missing or invalid. Delete the cookie on invalid tokens to prevent a redirect loop.

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 NextResponse.redirect(new URL('/login', request.url));
  try {
    await jwtVerify(token, SECRET);
    return NextResponse.next();
  } catch {
    const res = NextResponse.redirect(new URL('/login', request.url));
    res.cookies.delete('session'); // prevent redirect loop
    return res;
  }
}
export const config = { matcher: ['/dashboard/:path*'] };
Enter fullscreen mode Exit fullscreen mode

Redirect target must be a URL object, not a bare string.

How do I run A/B tests?

Check for a bucket cookie, assign one if absent, rewrite to the variant URL. The rewrite keeps the address bar showing the original URL — share links always go to control.

export function middleware(request: NextRequest) {
  if (!request.nextUrl.pathname.startsWith('/pricing')) return NextResponse.next();
  const existing = request.cookies.get('ab-pricing')?.value;
  const variant = existing ?? (Math.random() < 0.5 ? 'control' : 'variant-b');
  const url = request.nextUrl.clone();
  url.pathname = `/_experiments/pricing/${variant}/`;
  const res = NextResponse.rewrite(url);
  if (!existing) res.cookies.set('ab-pricing', variant, { maxAge: 86400 * 30, httpOnly: true });
  return res;
}
Enter fullscreen mode Exit fullscreen mode

Variants live at app/_experiments/pricing/control/ and app/_experiments/pricing/variant-b/ — the _ prefix makes them private routes.

What can Middleware read and write?

Operation API Notes
Read cookies request.cookies.get('name') Typed helpers built-in
Set cookies response.cookies.set(...) On any NextResponse
Forward headers NextResponse.next({ request: { headers } }) Pass locale, user ID downstream
Redirect (URL changes) NextResponse.redirect(url) Full URL object required
Rewrite (URL stays) NextResponse.rewrite(url) A/B tests, path aliases
Early response new NextResponse(body, { status }) Rate limit, maintenance mode

What should NOT go in Middleware?

Database queries — every matched request pays the round trip before the route runs; use a self-verifiable JWT instead. Heavy npm imports — increase cold-start time; keep middleware.ts to jose and nothing else avoidable. Network-dependent rate limiting — a Redis timeout becomes a timeout on every request; put rate limiting in a Route Handler.

FAQ

Q: Does Middleware run on static file requests?
A: Only on routes matching config.matcher. Without it, runs on everything including _next/static. Use the standard negative lookahead.

Q: What's the difference between redirect and rewrite?
A: redirect sends 307/308 and the browser navigates — address bar changes. rewrite serves different content while keeping the original URL. Auth → redirect; A/B tests → rewrite.

Q: Can Middleware read the POST body?
A: No — buffering breaks streaming and prevents the route from reading it. Decisions on cookies, headers, and URL only.

Q: Is Middleware still on the Edge Runtime in 2026?
A: Not on Vercel — it runs on Fluid Compute (standard Node.js). Constraint is latency budget, not API availability.


Originally published on devya.dev. Also on eng-ahmed.com. Built by Devya Solutions.

Top comments (0)