Middleware & API Routes

Implement middleware for auth, redirects, and build API endpoints

Middleware in Next.js

Middleware runs before a request is completed. It allows you to modify the response, redirect, rewrite URLs, or set headers based on the incoming request. Middleware runs on the Edge Runtime for fast execution.

🔧 Middleware Use Cases

  • ✅ Authentication & authorization
  • ✅ Redirects based on location/device
  • ✅ A/B testing & feature flags
  • ✅ Bot detection & rate limiting
  • ✅ Logging & analytics

Basic Middleware

// middleware.ts (root of your project)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Get the pathname
  const { pathname } = request.nextUrl;
  
  // Log every request
  console.log('Request to:', pathname);
  
  // Continue to the next middleware or route
  return NextResponse.next();
}

// Configure which paths run middleware
export const config = {
  matcher: [
    // Match all paths except static files
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};

Authentication Middleware

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value;
  const { pathname } = request.nextUrl;
  
  // Protected routes
  if (pathname.startsWith('/dashboard')) {
    if (!token) {
      // Redirect to login with return URL
      const loginUrl = new URL('/login', request.url);
      loginUrl.searchParams.set('from', pathname);
      return NextResponse.redirect(loginUrl);
    }
  }
  
  // Redirect logged-in users away from auth pages
  if (pathname.startsWith('/login') || pathname.startsWith('/signup')) {
    if (token) {
      return NextResponse.redirect(new URL('/dashboard', request.url));
    }
  }
  
  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/login', '/signup'],
};

Redirects & Rewrites

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

export function middleware(request: NextRequest) {
  const { pathname, searchParams } = request.nextUrl;
  
  // Redirect old URLs
  if (pathname === '/old-page') {
    return NextResponse.redirect(new URL('/new-page', request.url));
  }
  
  // Rewrite (URL stays the same, different page served)
  if (pathname === '/blog') {
    return NextResponse.rewrite(new URL('/news', request.url));
  }
  
  // Geo-based redirect
  const country = request.geo?.country || 'US';
  if (country === 'GB' && pathname === '/') {
    return NextResponse.rewrite(new URL('/uk', request.url));
  }
  
  // A/B testing
  const bucket = request.cookies.get('ab-test')?.value || 
    (Math.random() > 0.5 ? 'a' : 'b');
  
  if (pathname === '/pricing') {
    const response = NextResponse.rewrite(
      new URL(`/pricing-${bucket}`, request.url)
    );
    response.cookies.set('ab-test', bucket);
    return response;
  }
  
  return NextResponse.next();
}

Setting Headers

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

export function middleware(request: NextRequest) {
  // Clone the request headers
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-custom-header', 'my-value');
  
  // Create response with modified headers
  const response = NextResponse.next({
    request: { headers: requestHeaders },
  });
  
  // Set response headers
  response.headers.set('x-middleware-cache', 'no-cache');
  
  // CORS headers
  response.headers.set('Access-Control-Allow-Origin', '*');
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST');
  
  return response;
}

Route Handlers (API Routes)

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';

// GET /api/users
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const page = searchParams.get('page') || '1';
  
  const users = await db.user.findMany({
    skip: (parseInt(page) - 1) * 10,
    take: 10,
  });
  
  return NextResponse.json(users);
}

// POST /api/users
export async function POST(request: NextRequest) {
  const body = await request.json();
  
  const user = await db.user.create({
    data: body,
  });
  
  return NextResponse.json(user, { status: 201 });
}

// app/api/users/[id]/route.ts
// GET /api/users/:id
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const user = await db.user.findUnique({ where: { id } });
  
  if (!user) {
    return NextResponse.json(
      { error: 'User not found' },
      { status: 404 }
    );
  }
  
  return NextResponse.json(user);
}

// DELETE /api/users/:id
export async function DELETE(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  await db.user.delete({ where: { id } });
  return new NextResponse(null, { status: 204 });
}

Route Handler Features

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

export async function GET(request: NextRequest) {
  // Read cookies
  const cookieStore = await cookies();
  const token = cookieStore.get('token');
  
  // Read headers
  const headersList = await headers();
  const auth = headersList.get('authorization');
  
  // Set cookies in response
  const response = NextResponse.json({ success: true });
  response.cookies.set('session', 'abc123', {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 60 * 60 * 24 * 7, // 1 week
  });
  
  return response;
}

// Streaming response
export async function GET() {
  const stream = new ReadableStream({
    async start(controller) {
      for (let i = 0; i < 10; i++) {
        controller.enqueue(`Data chunk ${i}\n`);
        await new Promise(r => setTimeout(r, 100));
      }
      controller.close();
    },
  });
  
  return new Response(stream);
}

// Caching
export const revalidate = 60; // Cache for 60 seconds
export const dynamic = 'force-static'; // Always cache

📖 Middleware Documentation →

✅ Middleware & API Best Practices

  • • Keep middleware lightweight - it runs on every matched request
  • • Use matcher config to limit which paths run middleware
  • • Prefer Server Actions over API routes for mutations
  • • Use Route Handlers for webhooks and external API integrations
  • • Add proper error handling and status codes
  • • Consider Edge Runtime limitations (no Node.js APIs)