Middleware
Code running before the page – and therefore it has to be fast
A middleware.ts runs before every matching request. Good for redirects and rough access checks – not for database queries.
MiddlewareCode running before every matching request – for redirects, languages or access control.It runs in a restricted environment: no database access, no Node modules.→ sits in front of your pages. It may redirect, rewrite or set headers – before it is even decided which page gets rendered.
import { NextResponse, type NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const signedIn = request.cookies.has("session");
if (!signedIn) {
const target = new URL("/sign-in", request.url);
target.searchParams.set("next", request.nextUrl.pathname);
return NextResponse.redirect(target);
}
return NextResponse.next();
}
// Only for these addresses – important for speed
export const config = {
matcher: ["/account/:path*", "/admin/:path*"],
}; Without it the middleware runs on every request – including images, fonts and scripts. That costs time on every single call. Always narrow it to the addresses you actually mean.
What it can and cannot do
- +
Redirecting: not signed in → to the sign-in page
- +
Deriving language or region from the request
- +
A/B tests: set a cookie and rewrite internally
- +
Setting headers, a content security policy for instance
- −
Database queries – the environment simply cannot
- −
Full permission checks; it is too coarse for that
- −
Heavy computation – everything here delays every request
It checks whether a session cookie exists at all – not whether it is valid and what the person may do. The real check belongs in the page, the server action or the route handler. Middleware is the doorman, not the lock.
The restricted environment
Available: fetch, Web Crypto, URL, cookies, headers
Not available: Node modules (fs, path), database drivers,
long-running work
Reason: middleware runs in the edge environment, close to the
user and designed for short tasks. So in middleware you verify at most a signed token – Web Crypto can do that. Anything needing the database happens afterwards.
Rewriting instead of redirecting
// redirect: the address visibly changes
return NextResponse.redirect(new URL("/sign-in", request.url));
// rewrite: the address stays, something else is rendered internally
return NextResponse.rewrite(new URL("/en" + path, request.url)); ▸ In more depth: deriving the language from the request optional
const LANGS = ["en", "de"];
export function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;
// Already a language in the address? Then do nothing.
if (LANGS.some((l) => path.startsWith("/" + l + "/") || path === "/" + l)) {
return NextResponse.next();
}
const wanted = request.headers.get("accept-language") ?? "";
const lang = wanted.startsWith("de") ? "de" : "en";
return NextResponse.redirect(new URL("/" + lang + path, request.url));
}
export const config = {
matcher: ["/((?!_next|api|.*\\.).*)"], // no assets, no API
}; Someone who actively switches language does not want to be redirected again on their next visit. The usual approach is to store the choice in a cookie and check that before the header in the middleware.
Does it stick?
4 questions on this lesson. Wrong answers show up in your stats.