TechLead

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 executes before a request is completed, which makes it ideal for light request inspection, logging, or redirects.

// 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

Protect sensitive routes by checking cookies and redirecting unauthenticated users before a page renders.

// 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

Redirects change the URL in the browser, while rewrites serve different content without changing the visible URL.

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

You can read or modify headers to pass request context downstream or set response policies like caching and CORS.

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)

Route Handlers live in the App Router and let you build RESTful endpoints alongside your UI.

// 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

Route Handlers can read cookies and headers, stream responses, and configure caching behavior.

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)

Continue Learning