name: middleware-patterns description: 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. compatibility: Antigravity, Claude Code, Cursor metadata: version: "1.0" project: "stepleague"
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
| Issue | Cause | Fix |
|---|---|---|
| Middleware not running | Matcher excludes path | Update config.matcher |
| Redirect loops | Signin page matched | Add signin to public paths |
| Missing query params | Not preserving search | Use pathname + search |
| Edge runtime error | Using Node.js APIs | Use 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 checksupabase-patterns- Database operations (not in middleware)testing-patterns- Auth middleware tests inauth-middleware.test.ts