Middleware
Run code before requests complete — auth, redirects, headers, and i18n
What you'll learn
Implement middleware.ts at the project root
Configure matcher patterns for route targeting
Perform redirects and rewrites in middleware
Check authentication and protect routes
Manipulate cookies and request headers
Handle i18n routing with middleware
Middleware Basics
middleware.ts at the project root runs on every matched request:
// middleware.ts
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
export function middleware(request: NextRequest) {
console.log(`Request: ${request.method} ${request.nextUrl.pathname}`)
return NextResponse.next()
}
Matcher Config
Use the matcher export to target specific routes:
export const config = {
matcher: [
"/dashboard/:path*", // all dashboard routes
"/api/:path*", // all API routes
"/((?!_next/static|favicon.ico).*)", // exclude static assets
],
}
Authentication Check
Redirect unauthenticated users to login:
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
export function middleware(request: NextRequest) {
const token = request.cookies.get("session")?.value
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
const loginUrl = new URL("/login", request.url)
loginUrl.searchParams.set("redirect", request.nextUrl.pathname)
return NextResponse.redirect(loginUrl)
}
return NextResponse.next()
}
export const config = { matcher: "/dashboard/:path*" }
Redirects and Rewrites
// Permanent redirect
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === "/old-page") {
return NextResponse.redirect(new URL("/new-page", request.url), 308)
}
return NextResponse.next()
}
// Rewrite (serve content from a different URL without redirecting)
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === "/home") {
return NextResponse.rewrite(new URL("/", request.url))
}
}
308 vs 307 Redirect
Use 308 for permanent redirects (preserves request method, including POST). Use 307 for temporary redirects. NextResponse.redirect defaults to 307.
Cookies and Headers
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// Set a cookie
response.cookies.set("visitor", "true", {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 60 * 60 * 24, // 1 day
})
// Set a response header
response.headers.set("x-version", "1.0")
// Read request header
const country = request.geo?.country ?? "US"
response.headers.set("x-country", country)
return response
}
i18n Routing
import { match } from "@formatjs/intl-localematcher"
import Negotiator from "negotiator"
const locales = ["en", "es", "fr"]
const defaultLocale = "en"
function getLocale(request: NextRequest): string {
const negotiatorHeaders: Record<string, string> = {}
request.headers.forEach((value, key) => {
negotiatorHeaders[key] = value
})
const languages = new Negotiator({ headers: negotiatorHeaders }).languages()
return match(languages, locales, defaultLocale)
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (pathnameHasLocale) return NextResponse.next()
const locale = getLocale(request)
request.nextUrl.pathname = `/${locale}${pathname}`
return NextResponse.redirect(request.nextUrl)
}
See It In Action
Auth Middleware with Redirect
Code
tsx
1// middleware.ts2import { NextResponse } from "next/server"3import type { NextRequest } from "next/server"4 5export function middleware(request: NextRequest) {6const token = request.cookies.get("session")?.value7 8if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {9const loginUrl = new URL("/login", request.url)10loginUrl.searchParams.set("redirect", request.nextUrl.pathname)11return NextResponse.redirect(loginUrl)12}13 14return NextResponse.next()15}16 17export const config = {18matcher: ["/dashboard/:path*"]19}Reads the session cookie. If user has no session and tries /dashboard, redirects to /login with original path as redirect query param. Matcher scopes middleware to dashboard routes only.
Lesson Summary
Key Takeaways
middleware.ts runs before every matched request — ideal for auth, redirects, headers
matcher config controls which routes trigger middleware
NextResponse.redirect() and NextResponse.rewrite() provide URL manipulation
Cookies and headers can be read and modified in middleware
Middleware enables server-side i18n routing based on Accept-Language