Middleware, headers, cookies and sessions
Run code before a route renders, choose between a rewrite and a redirect, and protect a route group without trusting the client.
middleware.ts and matchers
// middleware.ts — project root, beside app/ or src/
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
if (pathname.startsWith('/admin') && !request.cookies.has('session')) {
const url = request.nextUrl.clone()
url.pathname = '/login'
url.searchParams.set('next', pathname)
return NextResponse.redirect(url)
}
const response = NextResponse.next()
response.headers.set('x-pathname', pathname)
return response
}
export const config = {
matcher: ['/admin/:path*', '/account/:path*'] // run only where it is needed
}- Middleware runs on every matched request before rendering — including static files and prefetches if the matcher is too broad, which costs latency on every navigation.
- It runs in the Edge runtime by default, so there are no Node APIs and no database driver that needs raw sockets. Decode a signed cookie; do not query the database.
- The matcher takes a path pattern or a regex string. When you cannot narrow it by path, exclude static assets explicitly.
- Middleware cannot produce a response body. It can rewrite, redirect, add request and response headers, and set cookies.
- Treat it as a cheap first filter rather than the authorisation layer: it can only inspect what the request carries, and every data path must check permissions again.
Rewrites, headers and cookies
export function middleware(request: NextRequest) {
const url = request.nextUrl.clone()
// redirect: the URL in the browser changes
if (url.pathname === '/old-guide') {
url.pathname = '/guides/intro'
return NextResponse.redirect(url, 308)
}
// rewrite: same URL, different content underneath
if (url.pathname.startsWith('/blog')) {
url.pathname = '/guides' + url.pathname.slice(5)
return NextResponse.rewrite(url)
}
const country = request.headers.get('x-vercel-ip-country')
const response = NextResponse.next()
response.cookies.set('country', country ?? 'unknown', {
httpOnly: true, sameSite: 'lax', secure: true, path: '/'
})
return response
}- Redirect when the URL should change for the user and for search engines. Rewrite when the URL is fine but the content comes from elsewhere — localisation, a test variant, a legacy path.
- Set cookies on the response object you return. A cookie set on
NextResponse.next()is visible to the server components rendering this request. - Mark a session cookie
httpOnly,secureandsameSite. A cookie readable by script can be exfiltrated by any third-party tag on the page. - Changing cookies in middleware triggers a re-render, which is why a theme or an experiment bucket decided there is applied on the very first paint.
- Config-level redirects and rewrites are evaluated before middleware. Keep permanent URL moves in
next.configso middleware stays short.
💡
A cookie proves what the browser sent, not who the user is. For anything that matters, store an opaque session id and resolve the real session on the server rather than trusting a value the client could have written.
Sessions and protecting a route group
// lib/session.ts
import 'server-only'
import { cache } from 'react'
import { cookies } from 'next/headers'
export const getSession = cache(async () => {
const token = (await cookies()).get('session')?.value
if (!token) return null
return verifyToken(token) // signature check, no round trip
})// app/(app)/layout.tsx — protect every route in the group
import { redirect } from 'next/navigation'
import { getSession } from '@/lib/session'
export default async function AppLayout({ children }) {
const session = await getSession()
if (!session) redirect('/login')
return <AppShell user={session.user}>{children}</AppShell>
}- Wrap the session lookup in React's
cacheso the layout, the page and the metadata function share one call per request instead of verifying three times. - A layout check protects rendering only. Route handlers and server actions under the same group are separate entry points and must check the session themselves.
import 'server-only'at the top of any module that touches secrets makes an accidental import from a client component fail the build instead of leaking.- Redirect rather than rendering a "please sign in" page for a protected route, so the URL matches the user's actual state.
- Sessions in cookies must be signed, or they are just strings the client controls. Set an expiry and rotate the token when privileges change.
FAQ
Can middleware talk to the database?
Not by default: it runs in the Edge runtime, where a TCP-based driver is unavailable. Verify a signed token or a short-lived session id there, and load the real user inside the route, where the Node runtime is available.
Is a layout check enough to protect a route?
No. A layout guards rendering, but a server action or route handler reachable from that area is invoked directly and never renders the layout. Check the session in every entry point that reads or writes data.
Related
Route handlers and API endpoints Dynamic routes, params and metadata
Last refreshed 2026-09-18.