Next.js Middleware läuft vor Abschluss eines Requests auf der Edge Runtime und ist ideal für Authentifizierung, Geo-Redirects, A/B-Tests und Rate Limiting.
Was ist Next.js Middleware?
Middleware ist eine Funktion, die vor dem Erreichen Ihrer Seite ausgeführt wird.
// Request flow in Next.js
//
// Client Request
// │
// ▼
// ┌─────────────┐
// │ Middleware │ ← Runs here (Edge Runtime)
// └─────────────┘
// │
// ▼
// ┌─────────────┐
// │ Page/API │ ← Your application code
// └─────────────┘
// │
// ▼
// Client ResponseGrundlegende Einrichtung
Erstellen Sie eine middleware.ts im Projektstamm.
// middleware.ts (project root)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Log every request
console.log(`[${request.method}] ${request.nextUrl.pathname}`);
// Continue to the next handler
return NextResponse.next();
}
// Only run on specific paths
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};Matcher-Konfiguration
Der Matcher bestimmt, welche Routen die Middleware auslösen.
// Match specific routes
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*'],
};
// Match all routes except static files and API
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
// Multiple matchers with different patterns
export const config = {
matcher: [
'/dashboard/:path*', // All dashboard routes
'/admin/:path*', // All admin routes
'/api/protected/:path*', // Protected API routes
'/profile', // Exact match
],
};
// Conditional matching with has/missing (Next.js 13.1+)
export const config = {
matcher: [
{
source: '/api/:path*',
has: [
{ type: 'header', key: 'authorization' },
],
},
],
};Authentifizierungs-Middleware
Schutz von Routen, die Authentifizierung erfordern.
JWT-Token-Verifizierung
JWT-Tokens direkt in der Middleware verifizieren.
// middleware.ts — JWT Authentication
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose';
const JWT_SECRET = new TextEncoder().encode(
process.env.JWT_SECRET!
);
const PUBLIC_PATHS = ['/login', '/register', '/forgot-password'];
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Skip public paths
if (PUBLIC_PATHS.some(path => pathname.startsWith(path))) {
return NextResponse.next();
}
// Get token from cookie or Authorization header
const token =
request.cookies.get('auth-token')?.value ||
request.headers.get('authorization')?.replace('Bearer ', '');
if (!token) {
// Redirect to login with return URL
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', pathname);
return NextResponse.redirect(loginUrl);
}
try {
// Verify JWT token
const { payload } = await jwtVerify(token, JWT_SECRET);
// Add user info to headers for downstream use
const response = NextResponse.next();
response.headers.set('x-user-id', payload.sub as string);
response.headers.set('x-user-role', payload.role as string);
return response;
} catch (error) {
// Token expired or invalid — redirect to login
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', pathname);
const response = NextResponse.redirect(loginUrl);
response.cookies.delete('auth-token');
return response;
}
}
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*', '/api/protected/:path*'],
};Rollenbasierte Zugriffskontrolle
Zugriffskontrolle basierend auf Benutzerrollen.
// Role-based access control in middleware
const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/admin': ['admin'],
'/dashboard': ['admin', 'editor', 'viewer'],
'/editor': ['admin', 'editor'],
'/api/admin': ['admin'],
};
function hasPermission(pathname: string, role: string): boolean {
for (const [route, roles] of Object.entries(ROUTE_PERMISSIONS)) {
if (pathname.startsWith(route)) {
return roles.includes(role);
}
}
return true; // No restriction for unmatched routes
}
export async function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value;
if (!token) return NextResponse.redirect(new URL('/login', request.url));
try {
const { payload } = await jwtVerify(token, JWT_SECRET);
const role = payload.role as string;
const { pathname } = request.nextUrl;
if (!hasPermission(pathname, role)) {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
return NextResponse.next();
} catch {
return NextResponse.redirect(new URL('/login', request.url));
}
}Weiterleitungsmuster
Ideal für bedingte Weiterleitungen.
Geo-basierte Weiterleitungen
Benutzer basierend auf Geolokation weiterleiten.
// Geo-based redirect (Vercel Edge)
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'US';
const city = request.geo?.city || 'Unknown';
// Redirect EU users to GDPR-compliant page
const EU_COUNTRIES = [
'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI',
'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU',
'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE',
];
if (EU_COUNTRIES.includes(country) &&
request.nextUrl.pathname === '/') {
return NextResponse.redirect(new URL('/eu', request.url));
}
// Add geo headers for downstream use
const response = NextResponse.next();
response.headers.set('x-country', country);
response.headers.set('x-city', city);
return response;
}Spracherkennung
Bevorzugte Sprache erkennen und weiterleiten.
// Locale detection middleware
const SUPPORTED_LOCALES = ['en', 'fr', 'de', 'es', 'ja', 'ko', 'zh'];
const DEFAULT_LOCALE = 'en';
function getPreferredLocale(request: NextRequest): string {
// Check cookie first (user preference)
const cookieLocale = request.cookies.get('locale')?.value;
if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale)) {
return cookieLocale;
}
// Parse Accept-Language header
const acceptLang = request.headers.get('accept-language') || '';
const preferred = acceptLang
.split(',')
.map(lang => lang.split(';')[0].trim().substring(0, 2))
.find(lang => SUPPORTED_LOCALES.includes(lang));
return preferred || DEFAULT_LOCALE;
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Check if locale is already in the path
const hasLocale = SUPPORTED_LOCALES.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (hasLocale) return NextResponse.next();
// Redirect to locale-prefixed path
const locale = getPreferredLocale(request);
const url = new URL(`/${locale}${pathname}`, request.url);
return NextResponse.redirect(url);
}Rate Limiting
Rate Limiting am Edge implementieren.
// Simple rate limiting with in-memory store
// Note: This works per-edge-node; use Redis for distributed rate limiting
const rateLimit = new Map<string, { count: number; resetTime: number }>();
const RATE_LIMIT = 100; // requests
const RATE_WINDOW = 60 * 1000; // per minute
function isRateLimited(ip: string): boolean {
const now = Date.now();
const record = rateLimit.get(ip);
if (!record || now > record.resetTime) {
rateLimit.set(ip, { count: 1, resetTime: now + RATE_WINDOW });
return false;
}
record.count++;
return record.count > RATE_LIMIT;
}
export function middleware(request: NextRequest) {
// Only rate-limit API routes
if (!request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.next();
}
const ip = request.headers.get('x-forwarded-for')?.split(',')[0] ||
request.ip ||
'127.0.0.1';
if (isRateLimited(ip)) {
return new NextResponse(
JSON.stringify({ error: 'Too many requests' }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': '60',
},
}
);
}
return NextResponse.next();
}Sliding Window
Sliding-Window-Algorithmus verwenden.
// Rate limiting with Upstash Redis (production-ready)
// npm install @upstash/ratelimit @upstash/redis
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
analytics: true,
prefix: 'api-ratelimit',
});
export async function middleware(request: NextRequest) {
if (!request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.next();
}
const ip = request.ip ?? '127.0.0.1';
const { success, limit, reset, remaining } = await ratelimit.limit(ip);
const response = success
? NextResponse.next()
: new NextResponse(JSON.stringify({ error: 'Rate limit exceeded' }), {
status: 429,
headers: { 'Content-Type': 'application/json' },
});
// Always set rate limit headers
response.headers.set('X-RateLimit-Limit', limit.toString());
response.headers.set('X-RateLimit-Remaining', remaining.toString());
response.headers.set('X-RateLimit-Reset', reset.toString());
return response;
}Response-Header und Sicherheit
Idealer Ort für Sicherheitsheader.
Sicherheitsheader
// Security headers middleware
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Prevent XSS attacks
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
);
// Prevent clickjacking
response.headers.set('X-Frame-Options', 'DENY');
// Prevent MIME type sniffing
response.headers.set('X-Content-Type-Options', 'nosniff');
// Enable HSTS
response.headers.set(
'Strict-Transport-Security',
'max-age=63072000; includeSubDomains; preload'
);
// Referrer policy
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
// Permissions policy
response.headers.set(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=()'
);
return response;
}CORS-Header
CORS auf Middleware-Ebene behandeln.
// CORS middleware for API routes
const ALLOWED_ORIGINS = [
'https://myapp.com',
'https://staging.myapp.com',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : '',
].filter(Boolean);
export function middleware(request: NextRequest) {
if (!request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.next();
}
const origin = request.headers.get('origin') || '';
const isAllowed = ALLOWED_ORIGINS.includes(origin);
// Handle preflight OPTIONS request
if (request.method === 'OPTIONS') {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': isAllowed ? origin : '',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
},
});
}
const response = NextResponse.next();
if (isAllowed) {
response.headers.set('Access-Control-Allow-Origin', origin);
response.headers.set('Access-Control-Allow-Credentials', 'true');
}
return response;
}A/B-Tests
Serverseitige A/B-Tests ohne Flackern.
// A/B Testing middleware
export function middleware(request: NextRequest) {
// Only A/B test the homepage
if (request.nextUrl.pathname !== '/') {
return NextResponse.next();
}
// Check if user already has a variant assigned
const variant = request.cookies.get('ab-variant')?.value;
if (variant) {
// Rewrite to the variant page without changing URL
return NextResponse.rewrite(
new URL(`/variants/${variant}`, request.url)
);
}
// Assign a random variant (50/50 split)
const newVariant = Math.random() < 0.5 ? 'control' : 'experiment';
const response = NextResponse.rewrite(
new URL(`/variants/${newVariant}`, request.url)
);
// Persist variant in cookie for 30 days
response.cookies.set('ab-variant', newVariant, {
maxAge: 60 * 60 * 24 * 30,
httpOnly: true,
sameSite: 'lax',
});
return response;
}URL-Rewriting
URLs umschreiben ohne Browser-URL zu ändern.
// Multi-tenant rewriting
// app.mysite.com → /tenants/app
// docs.mysite.com → /tenants/docs
export function middleware(request: NextRequest) {
const hostname = request.headers.get('host') || '';
const subdomain = hostname.split('.')[0];
if (subdomain && subdomain !== 'www' && subdomain !== 'mysite') {
return NextResponse.rewrite(
new URL(`/tenants/${subdomain}${request.nextUrl.pathname}`, request.url)
);
}
return NextResponse.next();
}
// Proxy API requests to external service
// /api/external/* → https://api.external-service.com/*
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/external/')) {
const path = request.nextUrl.pathname.replace('/api/external', '');
return NextResponse.rewrite(
new URL(`https://api.external-service.com${path}`)
);
}
return NextResponse.next();
}Middleware-Verkettung
Mehrere Middleware-Funktionen zusammensetzen.
// Composing multiple middleware functions
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
type MiddlewareFn = (
request: NextRequest,
response: NextResponse
) => NextResponse | Promise<NextResponse>;
function chain(
functions: MiddlewareFn[],
request: NextRequest
): Promise<NextResponse> {
return functions.reduce(
async (promise, fn) => {
const response = await promise;
return fn(request, response);
},
Promise.resolve(NextResponse.next())
);
}
// Individual middleware functions
const withLogging: MiddlewareFn = (request, response) => {
console.log(`[${request.method}] ${request.nextUrl.pathname}`);
return response;
};
const withSecurityHeaders: MiddlewareFn = (request, response) => {
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
return response;
};
const withAuth: MiddlewareFn = (request, response) => {
const token = request.cookies.get('auth-token')?.value;
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return response;
};
// Compose all middleware
export function middleware(request: NextRequest) {
return chain([withLogging, withSecurityHeaders, withAuth], request);
}Edge Runtime Einschränkungen
Die Edge Runtime hat einige Einschränkungen.
- Kein Dateisystemzugriff
- Keine nativen Node.js-Module
- Nur Web APIs
- Bundle-Größe auf 1MB begrenzt
- Keine DB-Verbindungen
- Begrenzte Ausführungszeit
// What you CAN use in Edge Runtime:
// ✅ fetch, Request, Response, Headers, URL
// ✅ TextEncoder, TextDecoder
// ✅ crypto.subtle (Web Crypto API)
// ✅ structuredClone
// ✅ atob, btoa
// ✅ URLPattern
// ✅ EdgeDB, Upstash, PlanetScale drivers
// What you CANNOT use:
// ❌ fs (file system)
// ❌ path, os, child_process
// ❌ pg, mysql2, mongoose (traditional DB drivers)
// ❌ bcrypt (use bcryptjs or Web Crypto instead)
// ❌ Large npm packages that exceed 1MB bundleDebugging
Debugging-Techniken für Middleware.
// Debugging middleware
export function middleware(request: NextRequest) {
// Log request details
console.log({
method: request.method,
url: request.url,
pathname: request.nextUrl.pathname,
cookies: Object.fromEntries(request.cookies.getAll().map(c => [c.name, c.value])),
headers: Object.fromEntries(request.headers),
geo: request.geo,
ip: request.ip,
});
// Return debug info in response headers (dev only)
const response = NextResponse.next();
if (process.env.NODE_ENV === 'development') {
response.headers.set('x-middleware-pathname', request.nextUrl.pathname);
response.headers.set('x-middleware-matched', 'true');
}
return response;
}Best Practices
- Middleware leichtgewichtig halten.
- Statische Dateien ausschließen.
- DB-Aufrufe vermeiden.
- Cache-Header setzen.
- Cookies für A/B-Tests.
- Fehlerbehandlung implementieren.
- Verschiedene Szenarien testen.
- Umgebungsvariablen verwenden.
Fazit
Next.js Middleware ist ein mächtiges Edge-Tool. Meistern Sie diese Muster für robuste Anwendungen.
FAQ
Wird Middleware bei jedem Request ausgeführt?
Standardmäßig ja, verwenden Sie den Matcher zur Einschränkung.
Kann man eine Datenbank in Middleware nutzen?
Nicht direkt. Verwenden Sie fetch zu einer API-Route.
Unterschied zwischen redirect und rewrite?
Redirect ändert die Browser-URL, Rewrite nicht.
Wie Fehler in Middleware behandeln?
Mit try-catch umschließen und passende Antwort zurückgeben.