Middleware: redirects and edge logic
Every part so far has run code inside a specific route. Middleware runs before any route matches at all — the earliest point in the request Next.js gives direct control over, which makes it the right place for things a full page render shouldn't be needed for.
A redirect for a renamed post
// middleware.ts — at the project root, alongside app/
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const REDIRECTS: Record<string, string> = {
"/posts/old-slug-name": "/posts/hello-app-router",
};
export function middleware(request: NextRequest) {
const redirectTo = REDIRECTS[request.nextUrl.pathname];
if (redirectTo) {
return NextResponse.redirect(new URL(redirectTo, request.url));
}
return NextResponse.next();
}middleware.ts has to live at the project root — not inside app/ — and its default export runs before Next.js resolves which route actually handles the request. NextResponse.redirect sends a real HTTP redirect immediately, without ever rendering the old page at all; NextResponse.next() lets the request continue to whichever route actually matches, unmodified.
Scoping which requests it runs on
export const config = {
matcher: ["/posts/:path*"],
};Without a matcher, middleware runs on every single request to the app — including static assets and API routes that likely don't need this check at all. matcher restricts it to only the paths that actually matter, here every route under /posts/, which keeps middleware's (small, but real) per-request cost from applying anywhere it doesn't need to.
Reading and setting cookies
export function middleware(request: NextRequest) {
const theme = request.cookies.get("theme");
const response = NextResponse.next();
if (!theme) {
response.cookies.set("theme", "dark");
}
return response;
}Middleware can read and set cookies directly on the response before any page even starts rendering — useful for something like a default preference that needs to be established on a visitor's very first request, before a Server Component would otherwise get the chance to check for it.
The edge runtime's real limits
Middleware runs on Next.js's edge runtime by default — a lighter, faster execution environment than a full Node.js server, but a genuinely restricted one: no direct database drivers that expect a Node.js environment, no arbitrary fs access, and a limited standard library compared to what a Server Component or Route Handler can use. Middleware is the right place for fast, simple checks — a redirect, a cookie read, a basic auth-token presence check — not for anything that needs to run a real database query or heavier logic; that belongs in the actual route it's protecting instead.
Putting expensive logic — a database call, a slow external API request — directly inside middleware. Every single matched request pays that cost before the page even starts rendering, and it's running in an environment not built to support it well in the first place. Middleware should stay fast and simple; heavier logic belongs in the Server Component or Route Handler it's protecting.
Next: styling — every example in this series has left class names unstyled. CSS Modules and global styles, the two approaches this project actually uses.