Middleware Patterns Skill
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.
SKILL.md
Full skill instructions
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
