DevToolBoxKOSTENLOS
Blog

Next.js Middleware: Authentifizierung, Weiterleitungen und Rate Limiting

14 Min.von DevToolBox

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 Response

Grundlegende 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 bundle

Debugging

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

  1. Middleware leichtgewichtig halten.
  2. Statische Dateien ausschließen.
  3. DB-Aufrufe vermeiden.
  4. Cache-Header setzen.
  5. Cookies für A/B-Tests.
  6. Fehlerbehandlung implementieren.
  7. Verschiedene Szenarien testen.
  8. 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.

𝕏 Twitterin LinkedIn
War das hilfreich?

Bleiben Sie informiert

Wöchentliche Dev-Tipps und neue Tools.

Kein Spam. Jederzeit abbestellbar.

Verwandte Tools ausprobieren

{ }JSON FormatterJWTJWT Decoder

Verwandte Artikel

Next.js App Router: Vollstaendiger Migrationsguide 2026

Meistern Sie den Next.js App Router mit diesem umfassenden Guide. Server Components, Datenabruf, Layouts, Streaming, Server Actions und schrittweise Migration vom Pages Router.

JWT-Authentifizierung: Vollstaendiger Implementierungsguide

JWT-Authentifizierung von Grund auf implementieren. Token-Struktur, Access- und Refresh-Tokens, Node.js-Implementierung, Client-seitige Verwaltung, Sicherheits-Best-Practices und Next.js-Middleware.

React Server Components: Kompletter Guide 2026

React Server Components meistern: Architektur, Datenabruf, Streaming und Migration.