Claude-skill-registry-data middleware-patterns

Next.js middleware patterns for protected routes, auth redirects, and URL handling. Use when working with middleware.ts, protected routes, or auth redirects. Keywords: middleware, protected routes, redirect, auth check, URL, searchParams.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry-data
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry-data "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/middleware-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-data-middleware-patterns && rm -rf "$T"
manifest: data/middleware-patterns/SKILL.md
source content

Middleware Patterns Skill

Overview

Next.js middleware runs before every request. StepLeague uses it for auth protection and redirects.


Middleware Location

src/
└── middleware.ts   ← Runs on every request matching config

Protected Routes Configuration

// src/middleware.ts

export const config = {
  matcher: [
    // Protected routes - require auth
    '/dashboard/:path*',
    '/league/:path*',
    '/settings/:path*',
    '/submit-steps/:path*',
    '/claim/:path*',
    
    // Admin routes
    '/admin/:path*',
  ],
};

Pattern 1: Auth Redirect with Full URL Preservation

Problem: Users lose query params after sign-in redirect.

// ❌ WRONG - Loses query params
const redirectUrl = new URL('/sign-in', request.url);
redirectUrl.searchParams.set('redirect', pathname);

// ✅ CORRECT - Preserves full URL
const redirectUrl = new URL('/sign-in', request.url);
const fullOriginalUrl = pathname + (request.nextUrl.search || '');
redirectUrl.searchParams.set('redirect', fullOriginalUrl);

Full Implementation:

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

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  
  // Check for auth token in cookies
  const hasAuthToken = request.cookies.has('sb-access-token') ||
                       request.cookies.getAll().some(c => c.name.includes('-auth-token'));
  
  if (!hasAuthToken) {
    // Preserve full URL including query params
    const signInUrl = new URL('/sign-in', request.url);
    const fullPath = pathname + (request.nextUrl.search || '');
    signInUrl.searchParams.set('redirect', fullPath);
    
    return NextResponse.redirect(signInUrl);
  }
  
  return NextResponse.next();
}

Pattern 2: Edge Runtime Compatibility

Critical: Don't use

@supabase/supabase-js
in middleware - it's not Edge-compatible.

// ❌ WRONG - Breaks Edge runtime
import { createServerSupabaseClient } from '@/lib/supabase/server';

export async function middleware(request: NextRequest) {
  const supabase = await createServerSupabaseClient();  // Crashes!
}

// ✅ CORRECT - Check cookies directly
export async function middleware(request: NextRequest) {
  const hasAuth = request.cookies.has('sb-access-token');
  // Simple check, no Supabase import
}

Pattern 3: Route-Based Logic

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  
  // Public routes - skip auth
  if (pathname.startsWith('/api/public') || 
      pathname === '/sign-in' ||
      pathname === '/sign-up') {
    return NextResponse.next();
  }
  
  // Admin routes - require superadmin
  if (pathname.startsWith('/admin')) {
    // Note: Full superadmin check happens in API/page
    // Middleware just ensures auth exists
    if (!hasAuth) {
      return NextResponse.redirect(new URL('/sign-in', request.url));
    }
  }
  
  // Protected routes - require any auth
  if (!hasAuth) {
    return redirectToSignIn(request);
  }
  
  return NextResponse.next();
}

Pattern 4: Header Injection

Add custom headers for downstream use:

export async function middleware(request: NextRequest) {
  const response = NextResponse.next();
  
  // Add pathname for layouts
  response.headers.set('x-pathname', request.nextUrl.pathname);
  
  // Add timestamp for debugging
  response.headers.set('x-middleware-ts', Date.now().toString());
  
  return response;
}

StepLeague Middleware Structure

// src/middleware.ts

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

const PROTECTED_PATHS = [
  '/dashboard',
  '/league',
  '/settings',
  '/submit-steps',
  '/claim',
  '/admin',
];

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  
  // Check if route needs protection
  const isProtected = PROTECTED_PATHS.some(p => pathname.startsWith(p));
  
  if (!isProtected) {
    return NextResponse.next();
  }
  
  // Check for auth cookie
  const authCookie = request.cookies.getAll()
    .find(c => c.name.includes('-auth-token'));
  
  if (!authCookie) {
    const signInUrl = new URL('/sign-in', request.url);
    const fullPath = pathname + (request.nextUrl.search || '');
    signInUrl.searchParams.set('redirect', fullPath);
    return NextResponse.redirect(signInUrl);
  }
  
  return NextResponse.next();
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
};

Debugging Middleware

Check if middleware runs:

export async function middleware(request: NextRequest) {
  console.log('[Middleware]', request.nextUrl.pathname);
  // ...
}

Check matcher config:

// Test your matcher at: https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher

Common Issues

IssueCauseFix
Middleware not runningMatcher excludes pathUpdate config.matcher
Redirect loopsSignin page matchedAdd signin to public paths
Missing query paramsNot preserving searchUse
pathname + search
Edge runtime errorUsing Node.js APIsUse only Edge-compatible code

Key File

File:

src/middleware.ts

This is the actual middleware implementation. See this file for the current protected paths and auth logic.


Related Skills

  • auth-patterns
    - Auth handling after middleware check
  • supabase-patterns
    - Database operations (not in middleware)
  • testing-patterns
    - Auth middleware tests in
    auth-middleware.test.ts