11 — Middleware and Route Structure — Code That Runs Before the Route
Cross-cutting concerns like auth, localization, and bot handling need a home that runs before any route does. The model that clicked: middleware is server code that runs before a request completes, before any route renders — it can rewrite, redirect, set headers, or read cookies [1][2]. Combined with the route matcher (which decides which routes middleware runs on) and a sensible app/ structure, middleware is the place for cross-cutting concerns like auth, localization, and bot handling.
Structuring routes — the skeleton
Before middleware makes sense, the routes themselves need structure. Structuring routes means organizing the project's file system to define the app's URLs — each file under app/ (or pages/) corresponds to a route, with the file's location and name directly determining the URL path [3]. The conventions worth using:
- Route groups — folders wrapped in parentheses, like (marketing) or (dashboard), that organize files without affecting the URL. Two route groups can each have their own root layout.
- Private folders — folders prefixed with _ are excluded from routing, useful for colocating utilities with routes.
- Colocation — keeping components, tests, and helpers next to the route that uses them, instead of a separate components/ sprawl.
A clear structure makes the route matcher in middleware tractable, because I'm matching against organized, named groups rather than an ad-hoc URL sprawl.
Middleware — the before-routes gate
Middleware runs on the server before a request is completed and before routes render [1][2]. Based on the incoming request, it can:
- Rewrite the request to a different internal path without changing the URL.
- Redirect the user to a different URL.
- Set request or response headers.
- Read and modify cookies.
- Respond directly, short-circuiting the route entirely.
The classic use cases the roadmap lists: authentication, authorization, redirects based on location (localization), bot handling, and security measures [2]. Middleware is the cross-cutting concerns layer — anything that should apply to many routes without living in each one.
// middleware.ts at the project root (or src/)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}The flow is a decision tree at the edge, before any route renders:
The route matcher — running middleware only where it matters
By default, middleware runs on every route, which is rarely what I want. The matcher config limits middleware to specific paths [4]:
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};A matcher defines patterns the request path must satisfy for the middleware to run [4]. This is the fine-grained control that keeps middleware cheap — auth checks run on protected routes only, not on every public page or static asset. Getting the matcher right is half the battle; an overly broad matcher runs middleware on requests that don't need it.
Cookies and headers — middleware's levers
Cookies and headers are the two request-time levers middleware uses most [5][6]:
- Cookies are small pieces of data stored on the user's machine (login state, preferences, A/B test assignment). In middleware I can read and modify them before the request hits a route [5], enabling auth checks, personalization, and experimentation at the edge.
- Headers control caching, security policies, or custom information passed to the client [6]. Setting a Content-Security-Policy or a custom x-region header across many routes is one middleware call instead of edits to each route.
How I use this
Middleware is where I put anything that must run before the route: auth checks (redirect to login if no session token), i18n locale detection (rewrite to the right locale prefix), A/B test assignment (set a cookie), and security headers. I keep the matcher tight — only the routes that actually need the check. The route structure underneath is organized into route groups so the matcher patterns read meaningfully. The discipline is: middleware for cross-cutting logic, the route itself for the page's own logic, and the matcher as the seam between them.
References
[1] Vercel, "Middleware (App Router)," Next.js Docs, 2024. [Online]. Available: https://nextjs.org/docs/app/api-reference/file-conventions/middleware
[2] Vercel, "Middleware — use cases," Next.js Docs, 2024. [Online]. Available: https://nextjs.org/docs/app/api-reference/file-conventions/middleware
[3] Vercel, "Project structure and organization," Next.js Docs, 2024. [Online]. Available: https://nextjs.org/docs/app/getting-started/project-structure
[4] Vercel, "Middleware — matcher," Next.js Docs, 2024. [Online]. Available: https://nextjs.org/docs/app/api-reference/file-conventions/middleware#matcher
[5] Vercel, "Middleware — using cookies," Next.js Docs, 2024. [Online]. Available: https://nextjs.org/docs/app/api-reference/file-conventions/middleware#using-cookies
[6] Vercel, "Middleware — setting headers," Next.js Docs, 2024. [Online]. Available: https://nextjs.org/docs/app/api-reference/file-conventions/middleware#setting-headers
[7] "Next.js 15 tutorial — Middleware," YouTube, 2024. [Video]. Available: https://www.youtube.com/watch?v=t1KTTZbqCm0
Knowledge check · Question 1 of 5
When does Next.js middleware run, relative to the route?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!