6. Middleware - A Detailed Guide
A document explaining middleware.ts - runs before a request reaches a page, used for auth gating, redirects, and NextAuth v5 integration.
On this page
- Table of Contents
- 1. When Does Middleware Run?
- 2. The middleware.ts File in Nova Shop
- 3. matcher - Which Requests Get Intercepted?
- 4. The NextAuth authorized Callback
- 5. Dual Auth: NextAuth + Backend Cookies
- 6. Layered Defense - Middleware Isn't Enough
- 7. Flow: an Unauthenticated User Visits /products
- 8. Edge Runtime Limitations
- 9. FAQ & Debugging
- 10. File Map
6. Middleware - A Detailed Guide
A document explaining middleware.ts - runs before a request reaches a page, used for auth gating, redirects, and NextAuth v5 integration.
Read in order: Section 1 → 2 (matcher) → 3 (authorized) → 4 (layered defense) → 5 (Nova) → 6 (FAQ).
Docs: Middleware · Authentication
Table of Contents
- When does middleware run?
- The
middleware.tsfile in Nova Shop matcher- which requests get intercepted?- The NextAuth
authorizedcallback - Dual auth: NextAuth + backend cookies
- Layered defense - middleware isn't enough
- Flow: an unauthenticated user visits
/products - Edge runtime limitations
- FAQ & debugging
- File map
1. When Does Middleware Run?
| Middleware | Server Component / Action | |
|---|---|---|
| Timing | Before rendering | During rendering / mutation |
| Runtime | Edge (default) | Node / Edge depending on the route |
| What it should do | Read cookies, lightweight redirects | Business logic, authFetch |
Don't call the heavy NestJS API from middleware - it adds latency to every single request.
2. The middleware.ts File in Nova Shop
// middleware.ts (project root)
import NextAuth from "next-auth";
import { authConfig } from "./auth.config";
export default NextAuth(authConfig).auth;
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)",
],
};
| Part | Meaning |
|---|---|
NextAuth(authConfig).auth | Middleware supplied by Auth.js - calls callbacks.authorized |
config.matcher | The path regex middleware does run on |
The auth logic lives in auth.config.ts → authorized({ auth, request }).
3. matcher - Which Requests Get Intercepted?
/((?!api|_next/static|_next/image|favicon.ico|.*\..*).*)
| Excluded | Example | Why |
|---|---|---|
api | /api/checkout, /api/stripe/webhook | Route Handlers manage their own auth |
_next/static | Built JS, CSS | Assets |
_next/image | Image optimization | Assets |
favicon.ico | Icon | Assets |
.*\..* | logo.png, file.css | Static files with an extension |
Consequences for Nova:
- A user navigating to
/products→ goes through middleware. - Stripe's
POST /api/stripe/webhook→ does not go through middleware (verified via signature instead). BuyNowButton'sfetch /api/checkout→ does not go through middleware.
4. The NextAuth authorized Callback
// auth.config.ts (abbreviated)
authorized({ auth, request: { nextUrl, cookies } }) {
const isLoggedIn = !!auth?.user;
const hasAccessToken = !!cookies.get("access_token")?.value;
const hasRefreshToken = !!cookies.get("refresh_token")?.value;
const accessExpired = isAccessTokenExpired(
cookies.get("access_expires_at")?.value,
);
const hasValidSession =
isLoggedIn &&
((hasAccessToken && !accessExpired) || hasRefreshToken);
const isProtectedRoute =
nextUrl.pathname.startsWith("/products") ||
nextUrl.pathname.startsWith("/customers") ||
nextUrl.pathname.startsWith("/cart");
if (isProtectedRoute) {
return hasValidSession; // false → redirect to the signIn page
}
if (hasValidSession && nextUrl.pathname === "/login") {
return Response.redirect(new URL("/products", nextUrl));
}
return true;
}
| Return value | Behavior |
|---|---|
true | Allow the request to continue |
false | Redirect to /login (pages.signIn) |
Response.redirect(...) | A custom redirect |
5. Dual Auth: NextAuth + Backend Cookies
Nova does not simply trust auth?.user:
Layer 1 - NextAuth session: the user has signed in (JWT session)
Layer 2 - Backend cookies: access_token, refresh_token, access_expires_at
Considered valid when:
isLoggedIn AND (access token still valid OR a refresh token exists)
Why? The NestJS API needs an Authorization: Bearer header built from access_token - the NextAuth session alone can't call the API.
Cookie details: 9. JWT Cookies and Dual Auth
6. Layered Defense - Middleware Isn't Enough
Per the Next.js Authentication Guide:
┌─────────────────────────────────────────┐
│ 1. Middleware → redirect early │
├─────────────────────────────────────────┤
│ 2. Server Action / Page → authFetch │
│ unauthorized() on 401 │
├─────────────────────────────────────────┤
│ 3. NestJS API → validate JWT │
└─────────────────────────────────────────┘
Why isn't middleware enough on its own?
- A user could call a Server Action directly (an internal POST) - bypassing the UI.
- The matcher excludes
/api- some endpoints don't go through middleware at all.
→ Nova uses authFetch + unauthorized() at layer 2. See 5. Data Fetching.
experimental.authInterrupts
// next.config.ts
experimental: { authInterrupts: true }
Enables Next.js 15's built-in support for unauthorized() as part of the auth flow.
7. Flow: an Unauthenticated User Visits /products
After a successful login: setAuthCookies + the NextAuth session → middleware lets the request through.
8. Edge Runtime Limitations
| Avoid in middleware | Prefer |
|---|---|
| Heavy DB queries | Reading cookies |
fs, some Node crypto APIs | Lightweight JWT decoding |
| Synchronous calls to NestJS | Checking whether a cookie exists |
auth.config.ts is kept edge-safe - the full providers live in auth.ts.
9. FAQ & Debugging
FAQ
Q: Middleware lets the request through, but the API still returns 401?
These are two separate layers - middleware passing ≠ the token is still valid. authFetch should refresh it, or trigger unauthorized().
Q: How do I add a new protected route, like /orders?
Add nextUrl.pathname.startsWith("/orders") to isProtectedRoute.
Q: Does the / page need login?
No, in Nova it's not included in isProtectedRoute - the landing page is public.
Debugging Checklist
- DevTools → Application → Cookies: check
access_token,refresh_token. - Does the path match the
matcher? - Temporarily log
nextUrl.pathname(dev only). - Compare against a 401 in the Network tab from
getProducts.
Exercises
- Why doesn't
/api/checkoutgo through middleware, yet remains safe? → Cart checkout callsgetCartSummaryserver-side using the cookie. - What's the difference between
authorizedreturningfalseversus a customredirect()?
10. File Map
middleware.ts → exports NextAuth().auth
auth.config.ts → authorized, session, pages.signIn
auth.ts → the full providers
next.config.ts → authInterrupts
app/unauthorized.tsx → the 401 UI (auth interrupt)
Next: 7. Authentication · 8. NextAuth v5
