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 & 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)