Get the FREE Ultimate OpenClaw Setup Guide →

nextjs-middleware-generator

npx machina-cli add skill Nembie/claude-code-skills/nextjs-middleware-generator --openclaw
Files (1)
SKILL.md
12.7 KB

Next.js Middleware Generator

Before generating any output, read config/defaults.md and adapt all patterns, imports, and code examples to the user's configured stack.

Generation Process

  1. Determine middleware requirements (auth, CORS, rate limiting, i18n, redirects)
  2. Generate middleware.ts at the project root
  3. Configure route matchers
  4. Add helper functions as needed

Base Middleware Structure

Create middleware.ts at the project root. Next.js only supports a single middleware file:

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

export function middleware(request: NextRequest) {
  // Middleware logic here
  return NextResponse.next();
}

export const config = {
  matcher: [
    // Match all routes except static files and Next.js internals
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
};

Auth Middleware

Protect routes by checking for a valid session:

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

const protectedRoutes = ['/dashboard', '/settings', '/profile'];
const authRoutes = ['/login', '/register'];

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const sessionToken = request.cookies.get('authjs.session-token')?.value;
  const isLoggedIn = !!sessionToken;

  // Redirect logged-in users away from auth pages
  if (isLoggedIn && authRoutes.some((route) => pathname.startsWith(route))) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  // Redirect unauthenticated users to login
  if (!isLoggedIn && protectedRoutes.some((route) => pathname.startsWith(route))) {
    const callbackUrl = encodeURIComponent(pathname);
    return NextResponse.redirect(new URL(`/login?callbackUrl=${callbackUrl}`, request.url));
  }

  return NextResponse.next();
}

Auth.js Integration

When using Auth.js, use its built-in middleware wrapper:

import { auth } from '@/auth';
import { NextResponse } from 'next/server';

export default auth((req) => {
  const isLoggedIn = !!req.auth;

  if (!isLoggedIn && req.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', req.url));
  }

  return NextResponse.next();
});

export const config = {
  matcher: ['/((?!api/auth|_next/static|_next/image|favicon.ico).*)'],
};

Role-Based Access

Check user role from JWT or session and restrict routes:

import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';

const roleRoutes: Record<string, string[]> = {
  '/admin': ['ADMIN'],
  '/dashboard/billing': ['ADMIN', 'BILLING'],
  '/dashboard': ['ADMIN', 'USER'],
};

async function getUserRole(request: NextRequest): Promise<string | null> {
  const token = request.cookies.get('authjs.session-token')?.value;
  if (!token) return null;

  try {
    const secret = new TextEncoder().encode(process.env.AUTH_SECRET);
    const { payload } = await jwtVerify(token, secret);
    return (payload.role as string) ?? null;
  } catch {
    return null;
  }
}

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

  for (const [route, allowedRoles] of Object.entries(roleRoutes)) {
    if (pathname.startsWith(route)) {
      const role = await getUserRole(request);

      if (!role) {
        return NextResponse.redirect(new URL('/login', request.url));
      }

      if (!allowedRoles.includes(role)) {
        return NextResponse.redirect(new URL('/unauthorized', request.url));
      }

      break;
    }
  }

  return NextResponse.next();
}

CORS Middleware

Add CORS headers for API routes:

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

const allowedOrigins = [
  'https://your-frontend.com',
  'http://localhost:3000',
];

function corsHeaders(origin: string | null) {
  const headers = new Headers();
  if (origin && allowedOrigins.includes(origin)) {
    headers.set('Access-Control-Allow-Origin', origin);
  }
  headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  headers.set('Access-Control-Max-Age', '86400');
  return headers;
}

export function middleware(request: NextRequest) {
  const origin = request.headers.get('origin');

  // Handle preflight OPTIONS requests
  if (request.method === 'OPTIONS') {
    return new NextResponse(null, {
      status: 204,
      headers: corsHeaders(origin),
    });
  }

  const response = NextResponse.next();

  // Add CORS headers to the response
  if (origin && allowedOrigins.includes(origin)) {
    response.headers.set('Access-Control-Allow-Origin', origin);
  }
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');

  return response;
}

export const config = {
  matcher: '/api/:path*',
};

Rate Limiting

Header-based rate limiting without external store (suitable for edge runtime):

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

const rateLimit = new Map<string, { count: number; resetTime: number }>();

const WINDOW_MS = 60_000; // 1 minute
const MAX_REQUESTS = 60;

function getRateLimitKey(request: NextRequest): string {
  return request.headers.get('x-forwarded-for')
    ?? request.headers.get('x-real-ip')
    ?? 'anonymous';
}

function checkRateLimit(key: string): { allowed: boolean; remaining: number } {
  const now = Date.now();
  const entry = rateLimit.get(key);

  if (!entry || now > entry.resetTime) {
    rateLimit.set(key, { count: 1, resetTime: now + WINDOW_MS });
    return { allowed: true, remaining: MAX_REQUESTS - 1 };
  }

  entry.count++;

  if (entry.count > MAX_REQUESTS) {
    return { allowed: false, remaining: 0 };
  }

  return { allowed: true, remaining: MAX_REQUESTS - entry.count };
}

export function middleware(request: NextRequest) {
  const key = getRateLimitKey(request);
  const { allowed, remaining } = checkRateLimit(key);

  if (!allowed) {
    return NextResponse.json(
      { error: 'Too many requests' },
      {
        status: 429,
        headers: {
          'Retry-After': '60',
          'X-RateLimit-Limit': String(MAX_REQUESTS),
          'X-RateLimit-Remaining': '0',
        },
      }
    );
  }

  const response = NextResponse.next();
  response.headers.set('X-RateLimit-Limit', String(MAX_REQUESTS));
  response.headers.set('X-RateLimit-Remaining', String(remaining));
  return response;
}

export const config = {
  matcher: '/api/:path*',
};

Redirect / Rewrite Rules

Permanent and Temporary Redirects

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

const permanentRedirects: Record<string, string> = {
  '/old-blog': '/blog',
  '/legacy-docs': '/docs',
};

const temporaryRedirects: Record<string, string> = {
  '/promo': '/campaigns/summer-2025',
};

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

  if (pathname in permanentRedirects) {
    return NextResponse.redirect(new URL(permanentRedirects[pathname], request.url), 308);
  }

  if (pathname in temporaryRedirects) {
    return NextResponse.redirect(new URL(temporaryRedirects[pathname], request.url), 307);
  }

  return NextResponse.next();
}

A/B Testing with Rewrites

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

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

  if (pathname === '/pricing') {
    const bucket = request.cookies.get('ab-bucket')?.value;
    const variant = bucket ?? (Math.random() < 0.5 ? 'a' : 'b');

    const response = NextResponse.rewrite(
      new URL(`/pricing/${variant}`, request.url)
    );

    if (!bucket) {
      response.cookies.set('ab-bucket', variant, {
        maxAge: 60 * 60 * 24 * 30, // 30 days
        httpOnly: true,
      });
    }

    return response;
  }

  return NextResponse.next();
}

i18n Routing

Detect locale from Accept-Language header and redirect:

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

const locales = ['en', 'it', 'de', 'fr', 'es'];
const defaultLocale = 'en';

function getPreferredLocale(request: NextRequest): string {
  const acceptLanguage = request.headers.get('accept-language') ?? '';
  const preferred = acceptLanguage
    .split(',')
    .map((lang) => lang.split(';')[0].trim().substring(0, 2))
    .find((lang) => locales.includes(lang));

  return preferred ?? defaultLocale;
}

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

  // Skip if path already has a locale prefix
  const hasLocale = locales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (hasLocale) return NextResponse.next();

  // Check for stored locale preference
  const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
  const locale = cookieLocale && locales.includes(cookieLocale)
    ? cookieLocale
    : getPreferredLocale(request);

  const response = NextResponse.redirect(
    new URL(`/${locale}${pathname}`, request.url)
  );

  response.cookies.set('NEXT_LOCALE', locale, {
    maxAge: 60 * 60 * 24 * 365,
    httpOnly: true,
  });

  return response;
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|api).*)'],
};

Composing Multiple Middlewares

Chain multiple middleware functions in a single middleware.ts:

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

type MiddlewareFn = (
  request: NextRequest,
  response: NextResponse
) => NextResponse | Response | undefined;

function composeMiddleware(...fns: MiddlewareFn[]) {
  return function middleware(request: NextRequest) {
    let response = NextResponse.next();

    for (const fn of fns) {
      const result = fn(request, response);

      // If middleware returns a redirect or error, stop the chain
      if (result instanceof Response && result !== response) {
        return result;
      }

      if (result) {
        response = result as NextResponse;
      }
    }

    return response;
  };
}

// Individual middleware functions
function withCors(request: NextRequest, response: NextResponse) {
  const origin = request.headers.get('origin');
  if (origin) {
    response.headers.set('Access-Control-Allow-Origin', origin);
    response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  }
  return response;
}

function withAuth(request: NextRequest, response: NextResponse) {
  const token = request.cookies.get('authjs.session-token')?.value;

  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return response;
}

function withHeaders(request: NextRequest, response: NextResponse) {
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('Referrer-Policy', 'origin-when-cross-origin');
  return response;
}

// Compose and export
export const middleware = composeMiddleware(withHeaders, withCors, withAuth);

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

Matcher Configuration

The config.matcher controls which routes the middleware runs on:

export const config = {
  // Single path
  matcher: '/dashboard/:path*',

  // Multiple paths
  matcher: ['/dashboard/:path*', '/api/:path*'],

  // Regex — exclude static files and Next.js internals
  matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],

  // Only API routes
  matcher: '/api/:path*',
};

Key rules:

  • Matchers must be string literals (no variables) for static analysis
  • Use :path* for wildcard segments
  • Always exclude _next/static, _next/image, and static assets to avoid unnecessary middleware execution

Completeness Check

After generating middleware, verify that: the middleware file is at the project root (not inside app/ or src/), the config.matcher excludes static files and _next internals, redirects use 307 (temporary) or 308 (permanent) status codes, CORS middleware handles both preflight OPTIONS and regular requests, auth middleware does not block the login page itself, and composed middlewares short-circuit correctly on redirects. If using Auth.js, verify the matcher excludes api/auth routes.

Asset

See assets/middleware-template/middleware.ts for a composable middleware starter template.

Source

git clone https://github.com/Nembie/claude-code-skills/blob/main/skills/nextjs-middleware-generator/SKILL.mdView on GitHub

Overview

Generates a single root middleware.ts that handles authentication, authorization, redirects, CORS, rate limiting, and internationalization. It adapts imports, patterns, and helpers from your configured stack, creating a cohesive guard for routes and ensuring scalable security right from project start.

How This Skill Works

The tool analyzes requested middleware needs (auth, CORS, rate limiting, i18n, redirects), creates middleware.ts at the project root, configures a robust matcher to skip Next.js internals, and adds helper functions (e.g., token checks, role verification, rate limits). It may also include an Auth.js integration snippet and a role-based access sample using JWTs, producing a ready-to-run guard with optional wrappers.

When to Use It

  • Protect protected routes (e.g., /dashboard, /settings) with session or token checks
  • Add CORS headers for API routes to allow trusted origins
  • Handle login/logout redirects to guide users to their intended pages
  • Set up role-based access control (RBAC) using JWT/session data
  • Configure i18n routing and locale-based redirects for multi-language sites

Quick Start

  1. Step 1: Create or update middleware.ts at the project root using the generator
  2. Step 2: Define required rules (auth, CORS, rate limit, i18n, redirects) and adjust the config.matcher
  3. Step 3: Run your Next.js app and verify protected routes, redirects, CORS headers, and locale routing

Best Practices

  • Start with a minimal, focused matcher to avoid perf penalties
  • Centralize auth/role logic in helper functions for reuse
  • Store tokens in secure, HttpOnly cookies and validate them server-side
  • Test both authenticated and unauthenticated paths across routes
  • Document and version the middleware to track Next.js updates and changes

Example Use Cases

  • Protect /dashboard by validating a session token and redirecting to /login if missing
  • Attach CORS headers to /api/* endpoints to permit a trusted origin
  • Redirect unauthenticated users to /login with a callbackUrl parameter for post-login navigation
  • Restrict /admin to users with ADMIN role, redirecting unauthorized users to /unauthorized
  • Route users to locale-specific content by configuring i18n matching and language redirects

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers