Skip to content
middleware-patterns logo

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

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