Next.js routing & middleware
Dynamic, catch-all, and optional catch-all segments
Bracketed folder names turn a URL segment into a parameter that page, layout, and route files read from params.
[slug]matches exactly one segment:/blog/introgives{ slug: "intro" }.[...slug](catch-all) matches one or more:/shop/a/bgives{ slug: ["a", "b"] }, and it does not match/shop.[[...slug]](optional catch-all) also matches the bare parent:/shopgives{ slug: undefined },/shop/agives{ slug: ["a"] }.
In Next 15 params is a Promise, so a Server Component awaits it; a Client Component reads it with use(params) or the useParams() hook.
// app/blog/[slug]/page.tsx
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params; // sync access still works but warns, and is going away
return <article>{slug}</article>;
}
Route handlers: route.ts on the Web Request and Response
A route.ts file exports one async function per HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS). It replaces pages/api and is built on the platform Request and Response, so you read the body with await request.json() or request.formData() rather than a bodyParser config. The request argument is a NextRequest, which adds .cookies and .nextUrl.
// app/api/posts/[id]/route.ts
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params; // context params is a Promise in Next 15
return Response.json({ id });
}
Two things bite in Next 15. A GET handler is no longer cached by default (it was static in Next 14), so opt back in with export const dynamic = "force-static" or a revalidate value. And page.ts and route.ts cannot both own the same segment, since each claims the whole route.
generateStaticParams prerenders dynamic routes
Export generateStaticParams to tell Next which parameter values to build ahead of time. It returns an array of param objects, one per route, runs at build before the matching pages render, and replaces the Pages Router getStaticPaths.
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((p) => ({ slug: p.slug })); // [{ slug: "intro" }, ...]
}
dynamicParams (default true) governs a value you did not return: render it on first request and cache it, or set dynamicParams = false to make unlisted paths 404. Return [] to build nothing ahead of time and render every path on demand.
Middleware runs before routing
middleware.ts sits beside app/ and exports one function that runs before a matched request reaches a route, on the Edge runtime by default. Use it for redirects, rewrites, and setting headers or cookies, returning NextResponse.next(), .redirect(), or .rewrite(). A config.matcher scopes which paths trigger it. Middleware is one step in a fixed order that every request follows:
A rewrite in that step masks the URL: the address bar keeps the requested path while Next serves the content of another route.
The middleware runtime and the matcher trap
In Next 15 middleware runs on the Edge runtime by default, so Node built-ins like fs and crypto, and npm packages that need them, are unavailable. The Node.js runtime became stable in 15.5 through export const config = { runtime: "nodejs" }, which unlocks the full API surface at the cost of Edge's fast start. Either way the code sits in the hot path of every matched navigation, so keep it light and move heavy work to the destination.
The matcher is the sharpest edge, with two rules:
- Values must be static string constants. Next reads them at build time, so a computed matcher is silently ignored and the middleware runs everywhere.
- With no matcher at all, middleware runs on every request, including
_next/static,_next/image, and files inpublic/. Header or redirect logic there can break your CSS, JS, and images.
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
Wrong "Next 16 renamed this, so I should write proxy.ts." Next 16 did rename middleware to proxy (defaulting to the Node runtime), but this module targets Next 15, where the file is middleware.ts on the Edge by default. Right ship middleware.ts for 15 and run the codemod when you upgrade.
Middleware is not an authorization boundary
Middleware looks like a natural gate: it runs before every matched request, so a cookie check there seems to guard everything behind it. The docs treat per-route verification as the real boundary, for two reasons.
A Server Action is an internal POST to the route of the page that uses it, not a distinct endpoint, so middleware covers it only when that page path is in the matcher, and the same action reused on an unmatched page slips through. A matcher edit can also drop coverage for a route with no error. A route handler under /api that the matcher does not list stays reachable directly, whatever the page middleware does.
So verify authentication and authorization inside each route handler and Server Function, close to the data. A cookie's presence is not proof of a valid session or the right role. Middleware stays useful for the coarse redirect and cheap edge work; as a single security boundary it is fragile, since a matcher gap or a request that never matches walks straight past it.
Parallel routes and the default.js failure mode
A @folder is a named slot passed as a prop to the shared parent layout, alongside the implicit children slot, and every slot renders at once. A slot is not a route segment, so app/@analytics/views/page.tsx resolves to /views, and if one slot at a level is dynamic then every slot at that level is dynamic.
default.js is the file that decides the failure mode. On soft (client) navigation Next keeps each slot's last active subpage even when it no longer matches the URL. On a hard navigation or refresh it cannot recover an unmatched slot, so it renders that slot's default.js, or returns a 404 when the file is missing. children is a slot too, so it also needs a default.js wherever recovery can fail.
Intercepting routes count segments, not folders
Intercepting routes load one route inside the current layout while keeping the URL, the mechanism behind a modal that stays shareable. The markers read like relative paths: (.) same level, (..) one level up, (..)(..) two up, (...) from the app root. What they count is the trap. They count route segments, and they skip @slot folders entirely.
The real modal recipe pairs a @modal slot with interception: @modal/(.)photo/[id]/page.tsx intercepts, @modal/default.tsx returns null, and a plain /photo/[id]/page.tsx is the full page. The marker is (.) even though photo sits inside @modal two folders deep, because a slot folder is not a segment and does not count. Soft navigation overlays the modal on the current page; a hard navigation, refresh, or shared link renders the standalone page, since interception happens only on client navigation.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.