Next.js Interview Prep
Routing

Middleware

The Gatekeeper That Runs Before Every Request

LinkedIn Hook

"Where do you put logic that needs to run before a page even starts rendering?"

That question catches most Next.js developers off guard. They reach for useEffect, or a layout component, or maybe an API route -- but the real answer is middleware.

Middleware is the invisible gatekeeper of your entire application. It intercepts every request at the edge, before any page component loads, before any layout renders, before any data fetching begins. It can redirect unauthenticated users, rewrite URLs for A/B testing, block traffic from specific regions, and enforce rate limits -- all in under a millisecond.

Yet in interviews, most candidates can't explain where the file goes, how the matcher works, or why middleware runs on the edge runtime instead of Node.js.

In Lesson 5.4, I break down everything you need to know about Next.js middleware -- from file placement to authentication guards to geolocation-based routing. This is the security checkpoint every request passes through.

Read the full lesson -> [link]

#NextJS #Middleware #WebDevelopment #Authentication #EdgeComputing #FrontendDevelopment #InterviewPrep


Middleware thumbnail


What You'll Learn

  • What middleware is and where it sits in the Next.js request lifecycle
  • Exactly where to place middleware.ts and why the location matters
  • How the matcher config controls which routes middleware intercepts
  • How to implement redirects, rewrites, and header modifications
  • Building authentication guards that protect entire route groups
  • Geolocation-based routing for international applications
  • The rate limiting concept and how middleware enables it
  • Edge runtime constraints and what you cannot do in middleware

The Airport Security Analogy

Think of your Next.js application as an international airport. Every passenger (HTTP request) arrives and wants to reach their gate (a specific page). But before they get anywhere near the gate, they pass through security screening -- that's middleware.

The security checkpoint doesn't serve food or sell souvenirs (it doesn't render pages). It has one job: inspect and decide. It checks your boarding pass (authentication token), verifies your destination (URL path), and makes a decision:

  • Let you through -- the request continues to the page as normal.
  • Redirect you -- "Your gate changed. Go to Terminal B instead." The browser navigates to a different URL.
  • Rewrite your path -- "You're actually going to Gate 42, but your boarding pass still says Gate 7." The URL stays the same, but the content comes from a different route.
  • Block you entirely -- "You can't enter. Go back to the check-in desk." Return a 403 or redirect to a login page.

And critically, this checkpoint runs at the edge -- it's physically close to the passenger, not back at airline headquarters (the origin server). Decisions are made in milliseconds before the request ever reaches your application server.

+------------------------------------------------------------------------+
|                     THE AIRPORT SECURITY MODEL                         |
+------------------------------------------------------------------------+
|                                                                        |
|  Passenger        Security          Decision          Destination      |
|  (Request)        Checkpoint        Point             (Page)           |
|                   (Middleware)                                          |
|                                                                        |
|  GET /dashboard   middleware.ts     Has auth token?                     |
|  ------>          ------>           /       \                           |
|                                   YES       NO                         |
|                                   /          \                         |
|                            Continue       Redirect                     |
|                            to /dashboard  to /login                    |
|                                                                        |
|  GET /blog/hello  middleware.ts     Match /blog/*?                      |
|  ------>          ------>           /       \                           |
|                                   YES       NO                         |
|                                   /          \                         |
|                            Rewrite to     Pass through                 |
|                            /en/blog/hello unchanged                    |
|                                                                        |
+------------------------------------------------------------------------+

File Placement — The One Rule Everyone Forgets

Middleware in Next.js is defined in a single file. The placement is non-negotiable:

your-project/
  middleware.ts       <-- HERE (project root, same level as app/)
  app/
    page.tsx
    dashboard/
      page.tsx
    api/
      route.ts
  next.config.js
  package.json

If you use the src/ directory convention:

your-project/
  src/
    middleware.ts     <-- HERE (inside src/, same level as app/)
    app/
      page.tsx
  next.config.js

The rule: middleware.ts lives at the root of your project, or inside src/ if you use that convention. It sits next to the app/ directory, never inside it. There is exactly one middleware file per project. You cannot have multiple middleware files or nest them inside route folders.

This single file handles all routes. You use the matcher config or conditional logic inside the function to control which routes it applies to.

// middleware.ts — the basic skeleton
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

// This function runs before every matched request
export function middleware(request: NextRequest) {
  // Inspect the request, make a decision, return a response
  return NextResponse.next(); // Continue to the page as normal
}

// Optional: control which routes trigger this middleware
export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
};

The Matcher Config — Controlling What Gets Intercepted

Without a matcher, middleware runs on every single request -- including static assets like images, fonts, and CSS files. That is almost never what you want. The matcher config tells Next.js which URL paths should trigger your middleware.

Basic Matcher Patterns

// Match a single path
export const config = {
  matcher: '/dashboard',
};

// Match a path and all its children
export const config = {
  matcher: '/dashboard/:path*',
};

// Match multiple paths
export const config = {
  matcher: ['/dashboard/:path*', '/account/:path*', '/api/:path*'],
};

// Match everything except static files and images
// This is the most common production pattern
export const config = {
  matcher: [
    // Match all request paths except:
    // - _next/static (static files)
    // - _next/image (image optimization files)
    // - favicon.ico (browser icon)
    // - public folder assets
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
};

Conditional Logic Inside Middleware

Sometimes the matcher is not granular enough. You can combine the matcher with conditional logic inside the function:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Skip middleware for public routes
  if (pathname.startsWith('/public') || pathname === '/') {
    return NextResponse.next();
  }

  // Apply auth logic only to protected routes
  if (pathname.startsWith('/dashboard') || pathname.startsWith('/account')) {
    const token = request.cookies.get('session-token')?.value;
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }

  // Apply locale detection to all other routes
  const locale = request.headers.get('accept-language')?.split(',')[0] || 'en';
  const response = NextResponse.next();
  response.headers.set('x-detected-locale', locale);
  return response;
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

Redirects — Sending Users to a Different URL

A redirect changes the URL in the browser. The user sees the new URL in the address bar. Search engines follow redirects and update their index. Use redirects when the destination is genuinely a different page.

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Permanent redirect (308) — old blog URLs to new structure
  // 308 preserves the HTTP method (GET stays GET, POST stays POST)
  if (pathname.startsWith('/blog/')) {
    const slug = pathname.replace('/blog/', '');
    return NextResponse.redirect(
      new URL(`/articles/${slug}`, request.url),
      308
    );
  }

  // Temporary redirect (307) — maintenance page
  if (pathname.startsWith('/shop') && process.env.MAINTENANCE_MODE === 'true') {
    return NextResponse.redirect(new URL('/maintenance', request.url));
  }

  // Redirect unauthenticated users to login
  if (pathname.startsWith('/dashboard')) {
    const token = request.cookies.get('auth-token');
    if (!token) {
      // Preserve the original URL so we can redirect back after login
      const loginUrl = new URL('/login', request.url);
      loginUrl.searchParams.set('callbackUrl', pathname);
      return NextResponse.redirect(loginUrl);
    }
  }

  return NextResponse.next();
}

Status codes matter in interviews:

  • 307 -- Temporary redirect. "This page is temporarily somewhere else." Browser does NOT cache it.
  • 308 -- Permanent redirect. "This page has moved forever." Browser caches it. Search engines transfer SEO ranking.
  • 301 / 302 -- Older equivalents, but can change the HTTP method. Prefer 307/308 in modern apps.

Rewrites — Changing the Content Without Changing the URL

A rewrite serves content from a different route without changing the browser URL. The user still sees the original URL in their address bar, but the page content comes from somewhere else. This is powerful for A/B testing, multi-tenant apps, and internationalization.

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const hostname = request.headers.get('host') || '';

  // A/B testing: 50% of users see version B of the landing page
  // The URL stays "/" but content comes from "/landing-b"
  if (pathname === '/') {
    const bucket = request.cookies.get('ab-bucket')?.value;
    if (bucket === 'b') {
      return NextResponse.rewrite(new URL('/landing-b', request.url));
    }
  }

  // Multi-tenant: subdomain-based routing
  // acme.example.com/pricing -> /tenants/acme/pricing
  const subdomain = hostname.split('.')[0];
  if (subdomain !== 'www' && subdomain !== 'example') {
    return NextResponse.rewrite(
      new URL(`/tenants/${subdomain}${pathname}`, request.url)
    );
  }

  // Internationalization: detect locale and rewrite to localized route
  // /about -> /en/about (content from localized folder, URL stays /about)
  const locale = request.cookies.get('locale')?.value || 'en';
  if (!pathname.startsWith(`/${locale}`)) {
    return NextResponse.rewrite(
      new URL(`/${locale}${pathname}`, request.url)
    );
  }

  return NextResponse.next();
}

Redirect vs Rewrite -- the interview distinction:

+------------------------------------------------------------------+
|  REDIRECT vs REWRITE                                              |
+------------------------------------------------------------------+
|                                                                   |
|  REDIRECT:                                                        |
|  User visits /old-page                                            |
|  Browser URL changes to /new-page                                 |
|  User sees /new-page in address bar                               |
|  Two HTTP round trips (302/307/308 + new request)                 |
|  Search engines update their index                                |
|                                                                   |
|  REWRITE:                                                         |
|  User visits /page                                                |
|  Browser URL stays /page                                          |
|  Content is served from /different-page                           |
|  One HTTP round trip (invisible to user)                          |
|  Search engines index the original URL                            |
|                                                                   |
+------------------------------------------------------------------+

Authentication Check — The Most Common Middleware Pattern

The number one use case for middleware in production is protecting routes behind authentication. Instead of adding auth checks inside every page component, middleware handles it in one centralized location.

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose'; // Edge-compatible JWT library

// Routes that require authentication
const protectedRoutes = ['/dashboard', '/account', '/settings', '/admin'];

// Routes that should redirect TO dashboard if user IS authenticated
const authRoutes = ['/login', '/register', '/forgot-password'];

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const token = request.cookies.get('auth-token')?.value;

  // Check if the token is valid (lightweight verification only)
  let isAuthenticated = false;
  if (token) {
    try {
      // Verify JWT signature without hitting a database
      // jose works on edge runtime (no Node.js crypto dependency)
      await jwtVerify(
        token,
        new TextEncoder().encode(process.env.JWT_SECRET)
      );
      isAuthenticated = true;
    } catch {
      // Token is invalid or expired — treat as unauthenticated
      isAuthenticated = false;
    }
  }

  // Protect dashboard and account routes
  const isProtectedRoute = protectedRoutes.some(route =>
    pathname.startsWith(route)
  );
  if (isProtectedRoute && !isAuthenticated) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('callbackUrl', pathname);
    return NextResponse.redirect(loginUrl);
  }

  // Redirect logged-in users away from login/register pages
  const isAuthRoute = authRoutes.some(route => pathname.startsWith(route));
  if (isAuthRoute && isAuthenticated) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  // Role-based access: check for admin routes
  if (pathname.startsWith('/admin') && isAuthenticated && token) {
    const { payload } = await jwtVerify(
      token,
      new TextEncoder().encode(process.env.JWT_SECRET)
    );
    if (payload.role !== 'admin') {
      return NextResponse.redirect(new URL('/unauthorized', request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
};

Why middleware for auth instead of checking in each page?

  1. Centralized -- one file protects all routes. No chance of forgetting to add an auth check to a new page.
  2. Runs before rendering -- the protected page never even starts to render. No flash of authenticated content.
  3. Edge-fast -- runs at the CDN edge, adding less than 1ms of latency in most cases.
  4. Works with static pages -- even SSG pages can be protected because middleware runs before the cached page is served.

Geolocation-Based Routing

Next.js middleware has access to geolocation data through the request.geo object (available on Vercel and some other edge platforms). This enables country-specific routing without client-side detection.

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const country = request.geo?.country || 'US'; // Default to US
  const city = request.geo?.city || 'Unknown';
  const region = request.geo?.region || 'Unknown';

  // Country-based content localization
  // Rewrite to country-specific version without changing URL
  const countryLocaleMap: Record<string, string> = {
    DE: 'de',
    FR: 'fr',
    JP: 'ja',
    BR: 'pt-br',
    ES: 'es',
  };

  const locale = countryLocaleMap[country] || 'en';
  const { pathname } = request.nextUrl;

  // Do not rewrite if already on a locale path or an API route
  if (pathname.startsWith('/api') || pathname.match(/^\/(en|de|fr|ja|pt-br|es)\//)) {
    return NextResponse.next();
  }

  // Rewrite to the localized version: /about -> /de/about for German users
  return NextResponse.rewrite(new URL(`/${locale}${pathname}`, request.url));
}

// Another pattern: block or redirect traffic from specific countries
export function middlewareWithGeoBlocking(request: NextRequest) {
  const country = request.geo?.country;

  // Regulatory compliance: redirect EU users to EU-specific pages
  const euCountries = ['DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'PL'];
  if (country && euCountries.includes(country)) {
    const { pathname } = request.nextUrl;
    if (!pathname.startsWith('/eu/')) {
      return NextResponse.rewrite(new URL(`/eu${pathname}`, request.url));
    }
  }

  return NextResponse.next();
}

Interview note: request.geo is platform-dependent. On Vercel, it is populated automatically from the CDN edge. On self-hosted Node.js, you would need to parse geolocation from a service like MaxMind or use a reverse proxy (nginx, Cloudflare) that injects geo headers. This is a common follow-up question.


Rate Limiting Concept

Middleware is the natural place for rate limiting because it intercepts requests before they consume server resources. True rate limiting requires a shared state store (like Redis), but you can implement a basic version using headers and cookies.

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

// NOTE: This is a simplified concept for interviews.
// Production rate limiting requires an external store (Redis, Upstash).
// In-memory stores do not work because edge functions are stateless.

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Only rate-limit API routes
  if (!pathname.startsWith('/api')) {
    return NextResponse.next();
  }

  // Identify the client (IP address or API key)
  const ip = request.headers.get('x-forwarded-for') || 'anonymous';
  const apiKey = request.headers.get('x-api-key');

  // In production, you would check a Redis counter here:
  // const count = await redis.incr(`rate-limit:${ip}`);
  // await redis.expire(`rate-limit:${ip}`, 60); // 60 second window
  // if (count > 100) { return rate limited response }

  // Example using Upstash Redis (edge-compatible):
  // import { Ratelimit } from '@upstash/ratelimit';
  // import { Redis } from '@upstash/redis';
  //
  // const ratelimit = new Ratelimit({
  //   redis: Redis.fromEnv(),
  //   limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10s
  // });
  //
  // const { success, limit, reset, remaining } = await ratelimit.limit(ip);
  //
  // if (!success) {
  //   return NextResponse.json(
  //     { error: 'Too many requests' },
  //     {
  //       status: 429,
  //       headers: {
  //         'X-RateLimit-Limit': limit.toString(),
  //         'X-RateLimit-Remaining': remaining.toString(),
  //         'X-RateLimit-Reset': reset.toString(),
  //       },
  //     }
  //   );
  // }

  // Add rate limit headers to successful responses
  const response = NextResponse.next();
  response.headers.set('X-RateLimit-Limit', '100');
  response.headers.set('X-RateLimit-Remaining', '99');
  return response;
}

The interview talking point: Middleware runs on the edge runtime, which is stateless. You cannot store a counter in a local variable because each invocation may run on a different edge node. This is why production rate limiting requires an external distributed store like Upstash Redis, which is designed for edge access with sub-millisecond latency.


Edge Runtime — What It Means and What You Cannot Do

This is the part that trips up the most interview candidates. Middleware does not run on the standard Node.js runtime. It runs on the Edge Runtime, a lightweight JavaScript environment designed for speed and global distribution.

+------------------------------------------------------------------------+
|  EDGE RUNTIME vs NODE.js RUNTIME                                       |
+------------------------------------------------------------------------+
|                                                                        |
|  EDGE RUNTIME (middleware)          NODE.js RUNTIME (API routes, pages) |
|  ---------------------------        ---------------------------------- |
|  Runs at CDN edge locations         Runs on origin server              |
|  Cold start: ~0ms                   Cold start: 50-250ms               |
|  Max execution: 25ms (Vercel)       Max execution: 60s+ (configurable) |
|  Max size: ~1-4MB                   No practical size limit            |
|  No Node.js APIs                    Full Node.js APIs                  |
|  No fs, path, child_process         fs, path, crypto, etc.             |
|  No native npm packages             Any npm package                    |
|  Web APIs only (fetch, crypto,      All Web APIs + Node.js APIs        |
|  TextEncoder, URL, Headers)                                            |
|  Cannot connect to databases        Can connect to databases           |
|  directly (no TCP sockets)          directly (TCP/TLS)                 |
|                                                                        |
+------------------------------------------------------------------------+

What You CAN Do in Middleware

// These all work in edge runtime:

// 1. Read and set cookies
const token = request.cookies.get('session')?.value;
const response = NextResponse.next();
response.cookies.set('visited', 'true', { maxAge: 86400 });

// 2. Read and set headers
const userAgent = request.headers.get('user-agent');
response.headers.set('x-custom-header', 'my-value');

// 3. Use the fetch API (call external services)
const res = await fetch('https://api.example.com/verify-token', {
  method: 'POST',
  body: JSON.stringify({ token }),
});

// 4. Use URL and URLSearchParams
const url = new URL(request.url);
url.searchParams.set('ref', 'middleware');

// 5. Use crypto (Web Crypto API, not Node.js crypto)
const encoder = new TextEncoder();
const data = encoder.encode('message');

// 6. Use edge-compatible libraries (jose for JWT, @upstash/redis, etc.)

What You CANNOT Do in Middleware

// These will FAIL in edge runtime:

// 1. File system access
import fs from 'fs';                    // ERROR: fs is not available
fs.readFileSync('./config.json');

// 2. Node.js native modules
import path from 'path';                // ERROR: path is not available
import { execSync } from 'child_process'; // ERROR

// 3. Database drivers that use TCP sockets
import { Client } from 'pg';            // ERROR: no TCP socket support
import mongoose from 'mongoose';         // ERROR

// 4. Heavy npm packages with native bindings
import sharp from 'sharp';              // ERROR: native C++ bindings
import bcrypt from 'bcrypt';            // ERROR: use bcryptjs instead

// 5. Node.js crypto (use Web Crypto API instead)
import crypto from 'crypto';            // ERROR in edge runtime
// Use: crypto.subtle.digest() (Web Crypto API) instead

The mental model: Edge runtime has the same APIs as a Service Worker or Cloudflare Worker. If it works in a browser's Service Worker, it works in middleware. If it requires Node.js, it does not belong in middleware.

Napkin AI Visual Prompt: "Dark gradient (#0a0e1a -> #111827). Split diagram: left side labeled 'Edge Runtime' in emerald green (#10b981) with icons for cookies, headers, fetch, URL, Web Crypto (checkmarks). Right side labeled 'Node.js Runtime' in purple (#8b5cf6) with icons for fs, database, TCP, native modules (checkmarks). Center dividing line with a middleware.ts label at the top. Title: 'Edge vs Node.js Runtime'. White monospace text."


The Complete Request Lifecycle with Middleware

Understanding where middleware sits in the full request lifecycle is a high-value interview answer:

+------------------------------------------------------------------------+
|  NEXT.js REQUEST LIFECYCLE                                             |
+------------------------------------------------------------------------+
|                                                                        |
|  1. Browser sends request                                              |
|     GET /dashboard                                                     |
|          |                                                             |
|          v                                                             |
|  2. CDN Edge receives request                                          |
|          |                                                             |
|          v                                                             |
|  3. MIDDLEWARE runs (edge runtime)  <--- You are here                  |
|     - Check auth token                                                 |
|     - Redirect / rewrite / modify headers                              |
|     - Decide: continue, redirect, or block                             |
|          |                                                             |
|          v                                                             |
|  4. Route matching (file-based routing)                                |
|     - Find the matching page.tsx / route.ts                            |
|          |                                                             |
|          v                                                             |
|  5. Static check: is this page cached?                                 |
|     - If SSG/ISR and cached: serve from cache                          |
|     - If SSR or cache miss: continue to server                         |
|          |                                                             |
|          v                                                             |
|  6. Server rendering (if needed)                                       |
|     - Execute server components                                        |
|     - Run data fetching                                                |
|     - Generate HTML                                                    |
|          |                                                             |
|          v                                                             |
|  7. Response sent to browser                                           |
|                                                                        |
+------------------------------------------------------------------------+

Middleware is step 3 -- it runs after the CDN edge receives the request but before any routing, caching, or rendering happens. This is why it can protect even statically generated pages.


Common Mistakes

1. Putting middleware.ts inside the app/ directory. Middleware must be at the project root (next to app/) or inside src/ (next to src/app/). Placing it inside app/ or a subfolder silently does nothing -- no error, no warning, just a middleware that never runs. This is the number one debugging headache.

2. Not using a matcher, then wondering why middleware is slow. Without a matcher, middleware runs on every request -- including every image, font, CSS file, and JavaScript chunk. This can add measurable latency. Always add a matcher that excludes _next/static, _next/image, and static assets.

3. Trying to use Node.js APIs in middleware. Importing fs, path, database drivers, or packages with native bindings crashes middleware. The edge runtime only supports Web APIs. Use edge-compatible alternatives: jose instead of jsonwebtoken, @upstash/redis instead of ioredis, bcryptjs instead of bcrypt.

4. Doing heavy computation or database queries in middleware. Middleware should be fast -- ideally under 1-5ms. It runs on every request. Expensive operations like database lookups, complex JWT decoding with multiple round-trips, or calling slow external APIs defeat the purpose. Keep middleware lightweight: verify a token signature, check a cookie, read a header. Move heavy logic to server components or API routes.

5. Forgetting that middleware cannot directly return page content. Middleware can redirect, rewrite, set headers, and set cookies. It cannot render HTML or return a React component. If you need to show a "you are blocked" page, redirect to a /blocked route that renders the page -- don't try to return HTML from middleware.

6. Using in-memory variables for rate limiting or state. Edge functions are stateless. A variable declared outside the middleware function is not shared across requests or edge nodes. Each invocation starts fresh. For any shared state (counters, session stores), use an external service.


Interview Questions

1. "Where does middleware.ts go in a Next.js project, and what happens if you put it in the wrong place?"

middleware.ts must be placed at the root of the project, at the same level as the app/ directory. If you use the src/ convention, it goes inside src/, next to src/app/. There is exactly one middleware file per project. If you put it inside app/, inside a route folder, or anywhere else, Next.js silently ignores it -- there is no build error or runtime warning, the middleware simply never executes. This is a common debugging trap because the failure mode is silent.

2. "Explain the difference between a redirect and a rewrite in middleware. When would you use each?"

A redirect (using NextResponse.redirect()) sends a 3xx HTTP response that tells the browser to navigate to a different URL. The address bar changes, and two round trips happen. Use redirects when the user should see a different URL -- for example, redirecting /old-blog/post to /articles/post permanently (308), or sending unauthenticated users to /login. A rewrite (using NextResponse.rewrite()) serves content from a different route without changing the browser URL. The user still sees the original URL but gets different content. Use rewrites for A/B testing (same URL, different landing pages), multi-tenant apps (subdomain routing), and internationalization (locale-prefixed content without locale in the URL).

3. "Why does Next.js middleware run on the edge runtime instead of the Node.js runtime? What are the trade-offs?"

The edge runtime enables middleware to run at CDN edge locations geographically close to the user, reducing latency to near zero. Since middleware runs on every matched request, even a few milliseconds of added latency would compound across all users. The trade-off is a restricted API surface: no file system access, no native Node.js modules, no TCP-based database connections, and a limited execution time (often 25ms on Vercel). You can only use Web APIs (fetch, crypto.subtle, TextEncoder, URL, Headers) and edge-compatible npm packages. This forces middleware to stay lightweight, which is by design -- it should make fast decisions (redirect, rewrite, modify headers), not do heavy computation.

4. "How would you implement authentication middleware that protects some routes but not others?"

I would define an array of protected route prefixes and check the request pathname against them. For the auth check itself, I would read a JWT from the cookies using request.cookies.get(), then verify the signature using an edge-compatible library like jose (not jsonwebtoken, which requires Node.js crypto). If verification fails or the cookie is missing, I would redirect to /login with a callbackUrl query parameter preserving the original destination. Public routes like /, /login, and /about would be excluded from the check. I would also handle the reverse case -- redirect already-authenticated users away from /login back to /dashboard. The matcher config would exclude static assets (_next/static, _next/image) to avoid running auth logic on non-page requests.

5. "Can middleware replace server-side auth checks in page components? Why or why not?"

Middleware is a first line of defense, not a complete replacement. It is excellent for route-level access control -- preventing unauthenticated users from reaching protected pages at all. However, middleware should only do lightweight verification (check that a token exists and has a valid signature). It should not do authorization checks that require database queries ("does this user have permission to edit this specific resource?") because edge runtime cannot connect directly to databases and the execution time is limited. Fine-grained permission checks should still happen in server components or server actions where you have full Node.js access. Think of middleware as the bouncer at the door (checks your ID) and server components as the concierge inside (checks your reservation for a specific room).


Quick Reference — Middleware Cheat Sheet

ConceptDetails
File locationProject root middleware.ts or src/middleware.ts
Number of filesExactly one per project
RuntimeEdge Runtime (not Node.js)
Runs whenBefore every matched request (before routing and rendering)
Matcher syntax'/path/:param*' for dynamic segments, regex in array
RedirectNextResponse.redirect(new URL('/target', request.url))
RewriteNextResponse.rewrite(new URL('/target', request.url))
ContinueNextResponse.next()
Read cookiesrequest.cookies.get('name')?.value
Set cookiesresponse.cookies.set('name', 'value', options)
Read headersrequest.headers.get('header-name')
Set headersresponse.headers.set('header-name', 'value')
Geolocationrequest.geo?.country, request.geo?.city (platform-dependent)
Auth libraryUse jose for JWT (edge-compatible), not jsonwebtoken
Rate limitingUse @upstash/ratelimit + @upstash/redis for edge stores
Cannot doFile system, native modules, TCP databases, heavy computation
Execution limit~25ms on Vercel, varies by platform
Status codes307 (temp redirect), 308 (permanent redirect), 429 (rate limit)
+----------------------------------------------------------+
|           MIDDLEWARE DECISION FLOW                         |
+----------------------------------------------------------+
|                                                           |
|  Request arrives                                          |
|       |                                                   |
|       v                                                   |
|  Does it match the matcher config?                        |
|       /           \                                       |
|     YES            NO --> Skip middleware, serve directly  |
|      |                                                    |
|      v                                                    |
|  Is the user authenticated?                               |
|       /           \                                       |
|     YES            NO --> Redirect to /login              |
|      |                                                    |
|      v                                                    |
|  Does the URL need rewriting?                             |
|  (A/B test, locale, multi-tenant)                         |
|       /           \                                       |
|     YES            NO                                     |
|      |              |                                     |
|      v              v                                     |
|  NextResponse    NextResponse                             |
|  .rewrite()      .next()                                  |
|                                                           |
+----------------------------------------------------------+

Prev: Lesson 5.3 -- Parallel Routes & Intercepting Routes Next: Lesson 6.1 -- Route Handlers (App Router) ->


This is Lesson 5.4 of the Next.js Interview Prep Course -- 8 chapters, 33 lessons.

On this page