---
title: "11 — Middleware and Route Structure — Code That Runs Before the Route"
uid: middleware-structuring
tags: ["roadmap:nextjs", "cookies", "route-matcher", "middleware", "headers", "nextjs"]
excerpt: "Middleware is server code that runs before any route renders — rewrites, redirects, headers, cookies — and the matcher decides which routes it guards."
date: 2026-08-13T03:28:01+0000
source: https://www.aveshina.my.id/en/blog/middleware-structuring
---

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:

```figure
<svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Request flow through middleware. A request enters middleware, which checks the matcher. If the path matches, middleware inspects cookies/headers and decides: rewrite (serve a different path, URL unchanged), redirect (send the browser elsewhere), set headers/cookies and pass through, or respond directly. If the path doesn't match, the request passes straight to the route.">
  <defs>
    <marker id="marrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <rect x="20" y="110" width="90" height="44" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="65" y="136" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">request</text>

    <path d="M110,132 L150,132" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#marrow)"/>

    <rect x="150" y="100" width="120" height="64" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="210" y="124" font-size="11" font-weight="700" fill="#422006" text-anchor="middle">middleware</text>
    <text x="210" y="142" font-size="10" fill="#475569" text-anchor="middle">matcher +</text>
    <text x="210" y="156" font-size="10" fill="#475569" text-anchor="middle">cookies/headers</text>

    <path d="M270,118 L340,40" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#marrow)"/>
    <path d="M270,132 L340,100" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#marrow)"/>
    <path d="M270,146 L340,160" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#marrow)"/>
    <path d="M270,160 L340,220" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#marrow)"/>

    <rect x="340" y="24" width="150" height="34" rx="6" fill="#fce7f3" stroke="#db2777"/>
    <text x="415" y="46" font-size="10" font-weight="700" fill="#500724" text-anchor="middle">rewrite (URL same)</text>

    <rect x="340" y="84" width="150" height="34" rx="6" fill="#fee2e2" stroke="#dc2626"/>
    <text x="415" y="106" font-size="10" font-weight="700" fill="#7f1d1d" text-anchor="middle">redirect</text>

    <rect x="340" y="144" width="150" height="34" rx="6" fill="#dcfce7" stroke="#16a34a"/>
    <text x="415" y="166" font-size="10" font-weight="700" fill="#052e16" text-anchor="middle">set headers → pass</text>

    <rect x="340" y="204" width="150" height="34" rx="6" fill="#e0e7ff" stroke="#6366f1"/>
    <text x="415" y="226" font-size="10" font-weight="700" fill="#1e1b4b" text-anchor="middle">respond directly</text>

    <path d="M490,161 L560,132" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#marrow)"/>
    <rect x="560" y="110" width="120" height="44" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="620" y="136" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">route renders</text>

    <text x="360" y="268" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">everything happens before the route — matcher keeps it off paths that don't need it</text>
  </g>
</svg>
```

## 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](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](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](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](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](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](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](https://www.youtube.com/watch?v=t1KTTZbqCm0)

```quiz
Q: When does Next.js middleware run, relative to the route?
- After the route renders
- Before the request completes and before any route renders
correct: 1
explain: Middleware runs on the server before a request is completed and before routes render. That's what makes it suited to cross-cutting concerns like auth and redirects.

Q: What does the matcher configuration do?
- It runs middleware on every route by default
- It limits middleware to specific path patterns, so it only runs where needed
correct: 1
explain: The matcher defines patterns the request path must satisfy for middleware to run, keeping middleware off routes (and static assets) that don't need it.

Q: Which two request-time levers does middleware most commonly use?
- Search params and route params
- Cookies and headers
correct: 1
explain: Cookies (auth state, preferences, A/B tests) and headers (caching, security policies) are middleware's primary read/modify levers.

Q: Route groups — folders wrapped in parentheses like (dashboard) — affect the URL. True or false?
- True — they add a URL segment
- False — they organize files without affecting the URL
correct: 1
explain: Route groups are purely organizational; they don't add a URL segment. They're useful for giving groups of routes their own layout without changing the URL shape.

Q: Middleware is the right place to put…
- a single page's data fetching
- cross-cutting concerns that apply to many routes, like auth checks and security headers
correct: 1
explain: Middleware handles cross-cutting logic that should run before many routes. Page-specific logic stays in the page; cross-cutting logic goes in middleware with a tight matcher.
```
