# Nextjs Middleware Generator

> Generate Next.js middleware for authentication, authorization, redirects, CORS, rate limiting, and internationalization. Use when asked to create middleware, protect routes, add CORS, handle redirects, or set up i18n routing.

- **Type:** Skill
- **Install:** `agentstack add skill-nembie-claude-code-skills-nextjs-middleware-generator`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Nembie](https://agentstack.voostack.com/s/nembie)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Nembie](https://github.com/Nembie)
- **Source:** https://github.com/Nembie/claude-code-skills/tree/main/skills/nextjs-middleware-generator

## Install

```sh
agentstack add skill-nembie-claude-code-skills-nextjs-middleware-generator
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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:

```typescript
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:

```typescript
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:

```typescript
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:

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

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

async function getUserRole(request: NextRequest): Promise {
  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:

```typescript
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):

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

const rateLimit = new Map();

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

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

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

const temporaryRedirects: Record = {
  '/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

```typescript
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()  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`:

```typescript
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:

```typescript
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 & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Nembie](https://github.com/Nembie)
- **Source:** [Nembie/claude-code-skills](https://github.com/Nembie/claude-code-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-nembie-claude-code-skills-nextjs-middleware-generator
- Seller: https://agentstack.voostack.com/s/nembie
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
