R
Rishtaara
Next.js: Zero to Full-Stack
Lesson 16 of 42Article16 min

Middleware, Proxy, NextRequest & NextResponse

For years, Next.js used a root middleware.ts (or src/middleware.ts) to run code before a request finishes routing — auth checks, redirects, rewrites, headers, A/B cookies.

middleware.ts and the move toward proxy

For years, Next.js used a root middleware.ts (or src/middleware.ts) to run code before a request finishes routing — auth checks, redirects, rewrites, headers, A/B cookies.

Next.js is evolving the naming and mental model toward proxy (proxy.ts) — same idea: intercept the request at the edge of your app, decide, then continue. Docs and versions may show middleware, proxy, or both. Learn the concept once; follow your installed Next.js docs for the exact filename.

Real-life example: Middleware / proxy is the security desk at a company gate. Every visitor passes the desk before entering an office (page).

  • Runs before layouts and pages for matched paths
  • Great for auth gates, geo redirects, feature flags
  • Keep it fast — no heavy database work if you can avoid it
  • Check your Next.js version: middleware.ts vs proxy.ts naming
Tip: Treat "middleware" and "proxy" as the same gatekeeper pattern. When you upgrade Next.js, rename/migrate only when the official guide says to.

NextRequest and NextResponse

The handler receives a NextRequest (URL, cookies, headers, geo hints). You return a NextResponse — next() to continue, redirect() to send elsewhere, or rewrite() to show another path while keeping the URL.

Real-life example: NextRequest is the visitor badge. NextResponse is the desk officer saying "go in", "go to reception", or "use the side entrance but keep your appointment card".

middleware.ts — protect /dashboard
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const token = request.cookies.get("session")?.value;

  if (pathname.startsWith("/dashboard") && !token) {
    const login = new URL("/login", request.url);
    login.searchParams.set("from", pathname);
    return NextResponse.redirect(login);
  }

  // Continue to the page
  const response = NextResponse.next();
  response.headers.set("x-rishtaara-path", pathname);
  return response;
}

// Only run on matching paths (keeps middleware lean)
export const config = {
  matcher: ["/dashboard/:path*", "/account/:path*"],
};
Rewrite example (same URL, different page)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const country = request.headers.get("x-vercel-ip-country") ?? "US";

  if (request.nextUrl.pathname === "/" && country === "IN") {
    // Browser still shows "/" but serves /in content
    return NextResponse.rewrite(new URL("/in", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/"],
};

Protecting routes — redirect vs rewrite

Redirect changes the browser URL — user sees /login. Rewrite keeps the URL but serves another route — useful for localization or maintenance pages without exposing internal paths.

For auth, prefer redirect to /login so the address bar matches reality. Store the original path in a query (?from=) so you can send them back after login.

Real-life example: Redirect is telling someone "go to the ticket counter" and walking them there. Rewrite is seating them in hall B while their ticket still says hall A.

Tip: Cookies set in middleware must follow your host's secure/sameSite rules. Test auth redirects in both local and production-like HTTPS.
Matcher tips
# Typical matcher ideas:
# /dashboard/:path*   — dashboard and nested
# /((?!_next|api|favicon.ico).*)  — exclude static/api (advanced)

# Keep matcher narrow. Running on every image request wastes edge time.