What Middleware Is For
Next.js middleware runs before a request reaches your route handler—at the edge, before any rendering. It's the right place for:
- Authentication checks
- Redirects based on user state
- Rate limiting
- A/B testing assignment
- Geolocation-based routing
- Request logging
Basic Setup
// middleware.ts (in root, next to app/)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
return NextResponse.next();
}
// Which paths it runs on
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|public/).*)',
],
};
Authentication Middleware
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyToken } from './lib/auth';
const PUBLIC_PATHS = ['/login', '/signup', '/api/auth', '/', '/pricing'];
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Allow public paths
if (PUBLIC_PATHS.some(path => pathname.startsWith(path))) {
return NextResponse.next();
}
// Check auth token
const token = request.cookies.get('session')?.value
?? request.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
}
try {
const payload = await verifyToken(token);
// Pass user data to route handlers via headers
const response = NextResponse.next();
response.headers.set('x-user-id', payload.userId);
response.headers.set('x-user-role', payload.role);
return response;
} catch {
const response = NextResponse.redirect(new URL('/login', request.url));
response.cookies.delete('session');
return response;
}
}
Role-Based Access
const ROLE_REQUIREMENTS: Record<string, string[]> = {
'/admin': ['admin', 'superadmin'],
'/api/admin': ['admin', 'superadmin'],
'/settings/billing': ['admin', 'owner'],
};
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const payload = await getTokenPayload(request);
if (!payload) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Check role requirements
for (const [path, requiredRoles] of Object.entries(ROLE_REQUIREMENTS)) {
if (pathname.startsWith(path)) {
if (!requiredRoles.includes(payload.role)) {
return NextResponse.redirect(new URL('/403', request.url));
}
}
}
return NextResponse.next();
}
Edge Rate Limiting
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
});
export async function middleware(request: NextRequest) {
const ip = request.ip ?? request.headers.get('x-forwarded-for') ?? 'anonymous';
// Only rate limit API routes
if (request.nextUrl.pathname.startsWith('/api/')) {
const { success, limit, remaining, reset } = await ratelimit.limit(ip);
if (!success) {
return new NextResponse('Too Many Requests', {
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
},
});
}
}
return NextResponse.next();
}
Geolocation and i18n Routing
export function middleware(request: NextRequest) {
// Next.js provides geo data at the edge
const country = request.geo?.country ?? 'US';
const locale = request.headers.get('accept-language')?.split(',')[0] ?? 'en';
// Redirect EU users to GDPR-compliant subdomain
const EU_COUNTRIES = ['DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'SE'];
if (EU_COUNTRIES.includes(country) && !request.nextUrl.hostname.includes('eu.')) {
const url = request.nextUrl.clone();
url.hostname = `eu.${url.hostname}`;
return NextResponse.redirect(url);
}
return NextResponse.next();
}
Passing Data to Route Handlers
// Middleware sets headers
const response = NextResponse.next();
response.headers.set('x-user-id', payload.userId);
response.headers.set('x-org-id', payload.orgId);
// Route handler reads them
export async function GET(request: NextRequest) {
const userId = request.headers.get('x-user-id')!;
const orgId = request.headers.get('x-org-id')!;
const data = await db.projects.findMany({ where: { orgId } });
return Response.json(data);
}
Performance Notes
Middleware runs at the edge (Vercel Edge Network or Node.js). Keep it fast:
- No database queries (use KV stores like Upstash Redis)
- No heavy computation
- JWT verification is fine (crypto is fast)
- Keep the matcher narrow—don't run on static assets
Middleware that adds 5ms to every request adds up. Target < 10ms total.
Auth middleware with JWT verification, role-based access, and rate limiting: Whoff Agents AI SaaS Starter Kit.
Top comments (0)