What Is Middleware? A Beginner-Friendly Guide
Understand middleware from first principles, why SaaS apps use it, and how our Next.js middleware works with i18n and request IDs — plus how to customize it.
Middleware in one sentence
Middleware is code that runs between an incoming request and your final handler, so you can inspect, redirect, or add headers before the page or API runs.
Middleware 101 (Beginner Perspective)
Picture a request as a car on a road. Middleware is the checkpoint: every request passes through it. At that checkpoint you can read the request, make a decision, and optionally change the response (or even stop the request early).
Common web app uses:
- Authentication gates: check if a user is signed in, otherwise redirect.
- Internationalization (i18n): route users to the right locale based on URL or preferences.
- Logging and tracing: attach a
request_idso logs can be correlated end-to-end. - Feature flags / A/B testing: bucket users into experiments early.
- Security and rate limiting: block abusive traffic or enforce headers.
In Next.js, middleware lives in a special middleware.ts file and runs at the edge (very early and very fast) for matched paths. It can rewrite/redirect requests, read or set headers, and short-circuit responses before your route handlers execute.
Why Middleware Helps (And When You Need It)
Middleware centralizes cross-cutting concerns (logic that should apply to many routes) so you do not repeat it in every page or API handler:
- One place to enforce rules: fewer chances to forget an auth or locale check.
- Consistent observability: every request gets the same
request_idand headers. - Faster decisions: redirect or deny early, saving server work.
- Cleaner handlers: business logic stays in pages and APIs, not in plumbing.
SaaS scenarios where middleware is especially useful:
- Localized apps that must keep
/:locale/...URLs consistent. - Apps that rely on per-request correlation IDs for logs and tracing.
- Gatekeeping for paid features or admin areas (with careful cookie handling).
- Experimentation (A/B), geo routing, and header normalization.
Our Middleware: What It Does Today
We ship a minimal, safe default in src/middleware.ts that composes i18n routing from next-intl with lightweight request ID propagation.
Key behaviors:
- Locale routing: delegates to
next-intlusing our routing config (src/i18n/routing.ts,src/i18n/locale.ts). This keeps URLs like/en/...,/fr/...consistent and enables optional locale detection. - Request ID header: ensures every response carries
x-request-idfor log correlation. If the incoming request already has one, we reuse it; otherwise we generate a new UUID. - Conservative matcher: runs on localized pages and general app routes; intentionally skips assets, Next internals, API routes, and
admin.
Current source (trimmed for clarity):
// src/middleware.ts
import createMiddleware from 'next-intl/middleware';
import { NextRequest } from 'next/server';
import { routing } from '@/i18n/routing';
const intlMiddleware = createMiddleware(routing);
export default function middleware(request: NextRequest) {
const existing = request.headers.get('x-request-id');
const requestId = existing || crypto.randomUUID();
const res = intlMiddleware(request);
// Visibility only; do not mutate request cookies/headers.
res.headers.set('x-request-id', requestId);
return res;
}
export const config = {
matcher: [
'/',
'/(en|en-US|zh|zh-CN|zh-TW|zh-HK|zh-MO|ja|ko|ru|fr|de|ar|es|it)/:path*',
'/((?!api|_next|_vercel|admin|.*\\..*).*)',
],
};Locale routing is configured here:
// src/i18n/routing.ts
import { defaultLocale, localeDetection, localePrefix, locales } from './locale';
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({ locales, defaultLocale, localePrefix, localeDetection });And locales are declared here (edit to add/remove locales):
// src/i18n/locale.ts
export const locales = ['en', 'zh', 'es', 'fr', 'ja'];
export const defaultLocale = 'en';
export const localePrefix = 'always';
export const localeDetection = process.env.NEXT_PUBLIC_LOCALE_DETECTION === 'true';Notes:
- We deliberately avoid changing request headers/cookies in middleware to keep Better Auth stable. We only set a response header.
- The matcher excludes
api,_next, assets, andadminfrom middleware by default. Adjust this if you need middleware there.
How To Customize (Safely)
Before you customize, confirm the logic truly belongs in middleware (cross-cutting, early decision) instead of a specific route. When it does belong in middleware, use patterns like these:
Add custom headers
export default function middleware(request: NextRequest) {
const res = intlMiddleware(request);
res.headers.set('x-feature-flag', 'on');
return res;
}Redirect or rewrite paths
import { NextResponse } from 'next/server';
export default function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname === '/old') {
return NextResponse.redirect(new URL('/new', request.url));
}
return intlMiddleware(request);
}Include/exclude routes via matcher
export const config = {
matcher: [
'/',
// Add admin back under middleware by removing it from the negative lookahead
// or by explicitly matching the segment:
'/admin/:path*',
'/(en|fr|ja|zh|es)/:path*',
'/((?!api|_next|_vercel|.*\\..*).*)',
],
};Use existing request IDs inside handlers
// In a Node route handler
import { headers } from 'next/headers';
export async function GET() {
const h = headers();
const request_id = h.get('x-request-id');
// attach to logs, db traces, etc.
return new Response('ok');
}Internationalization tweaks
- Change
NEXT_PUBLIC_LOCALE_DETECTIONtotrueto enable auto detection. - Update
src/i18n/locale.tsto add/remove locales and names. - Keep
content/docs,messages/, andsrc/i18nin sync when adding a new locale.
Auth and sensitive flows
- If you add auth gating in middleware, avoid mutating cookies directly; prefer redirects and read-only checks.
- Verify that protected API routes are gated at the handler level too — middleware is a convenience, not your only guard.
When Not To Use Middleware
- Route-specific logic that does not apply broadly.
- Heavy async work (DB calls) that belongs in the handler.
- Transforming large request bodies (middleware cannot read the body anyway).
Keep middleware fast and focused. If it grows complex, move specialized logic into service functions and call them from handlers instead.
TL;DR
- Middleware runs early and centrally — perfect for i18n, headers, redirects, and light guards.
- Our default middleware handles locale routing and sets
x-request-idfor tracing. - Customize via
config.matcher, response headers, and safe redirects; avoid touching auth cookies. - Keep it small, fast, and consistent across locales.
What Is Next.js? The Beginner’s Guide
Why Next.js suits SaaS: routing, SSR/SSG, and API routes in one repo — a beginner-friendly, full-length guide.
Frontend vs Backend vs Full‑Stack for SaaS Beginners
A beginner-friendly guide to understanding frontend, backend, and full‑stack development in the context of SaaS, with a relatable analogy and examples from a modern web stack.