C
Next.js/Routing/Lesson 11

Middleware — Intercepting Every Request for Auth, Redirects, and Header Manipulation

30 min·theory
This chapter
3/3
TypeScript

Middleware — Intercepting Every Request for Auth, Redirects, and Header Manipulation

💡 Why Learn This? — So You Don't Have to Write if(!session) in Every Page Component

🎯 Placing 'redirect to login if no session' code on every protected page is tedious — putting it in middleware in one place is all you need.
💼 Runs on the Edge Runtime → blocks requests before they reach page components. Faster response.
Use the matcher config to precisely control which routes the middleware applies to.
🔗 A/B testing, country-based redirects, bot blocking, adding response headers — all the logic needed just before routing.
📈 codemaster40's Next.js app also uses [`src/middleware.ts`](src/middleware.ts) to handle protected routes like `/dashboard` and `/admin`.
🏢 실무에서는
This project's middleware.ts does exactly that — it validates the session when entering personalized routes like `/dashboard`, `/bookmark`, and `/memo-notes`, and redirects to `/login?next=...` if there is none. Page components can then be written assuming a session always exists.

middleware.ts · matcher · NextResponse

1. Location and Signature

code
src/middleware.ts  ← same level as src/app (or project root)
ts
import { NextResponse, type NextRequest } from 'next/server';

export function middleware(req: NextRequest) {
  // runs on every request
  return NextResponse.next();  // just pass through
}

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*'],
};
  • Runs on the Edge Runtime (some Node APIs are restricted).
  • Only activates for paths listed in matcher.

2. matcher Patterns

ts
export const config = {
  matcher: [
    '/dashboard/:path*',           // all /dashboard/*
    '/((?!api|_next|favicon).*)',  // everything except api/_next/favicon
  ],
};

3. Four Key Behaviors

ts
// (a) pass through
return NextResponse.next();

// (b) rewrite to another path (URL stays the same, only internal routing changes)
return NextResponse.rewrite(new URL('/landing', req.url));

// (c) redirect (changes the URL itself)
return NextResponse.redirect(new URL('/login', req.url));

// (d) manipulate response headers
const res = NextResponse.next();
res.headers.set('x-custom', 'value');
return res;

4. In Practice — Auth Middleware

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

export function middleware(req: NextRequest) {
  const session = req.cookies.get('session')?.value;

  if (!session) {
    const loginUrl = new URL('/login', req.url);
    loginUrl.searchParams.set('next', req.nextUrl.pathname);
    return NextResponse.redirect(loginUrl);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/bookmark/:path*', '/memo-notes/:path*'],
};

5. Manipulating Response Cookies and Headers

ts
export function middleware(req: NextRequest) {
  const res = NextResponse.next();

  // set response cookies
  res.cookies.set('lastVisit', new Date().toISOString(), {
    httpOnly: true,
    secure: true,
  });

  // add security headers
  res.headers.set('X-Frame-Options', 'DENY');
  res.headers.set('X-Content-Type-Options', 'nosniff');

  return res;
}

6. Edge Runtime Constraints

  • Node-only modules like fs, net, and dns are not available.
  • Response time should stay under 25ms (Vercel guideline).
  • No heavy database calls — only fast, lightweight checks.
💻 🅰️ Per-Page Auth Checks — Code Duplication Explosion
// ❌ Without middleware — repeated authentication code on every page

// 📁 app/dashboard/page.tsx
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

export default async function Dashboard() {
  const session = cookies().get('session')?.value;
  if (!session) redirect('/login?next=/dashboard');
  // ... page body
}

// 📁 app/bookmark/page.tsx
export default async function Bookmark() {
  const session = cookies().get('session')?.value;
  if (!session) redirect('/login?next=/bookmark');
  // ... same code
}

// 📁 app/memo-notes/page.tsx
export default async function MemoNotes() {
  const session = cookies().get('session')?.value;
  if (!session) redirect('/login?next=/memo-notes');
  // ... same code again
}

// Disadvantages:
// - 4 lines duplicated on every page
// - Easy to forget when adding new pages
// - If authentication policy changes, all pages need modification
// - Validation occurs only after page component executes → slight compile/transfer overhead
💻 🅱️ Middleware — Handle All Protected Routes in One Place
// ✅ Integrated with middleware — page components have no authentication code

// 📁 src/middleware.ts
import { NextResponse, type NextRequest } from 'next/server';

const PROTECTED_ROUTES = ['/dashboard', '/bookmark', '/memo-notes'];
const ADMIN_ROUTES = ['/admin'];

export function middleware(req: NextRequest) {
  const session = req.cookies.get('session')?.value;
  const path = req.nextUrl.pathname;

  // 1. Protected routes → if not logged in, redirect to login page
  if (PROTECTED_ROUTES.some(p => path.startsWith(p))) {
    if (!session) {
      const loginUrl = new URL('/login', req.url);
      loginUrl.searchParams.set('next', path);
      return NextResponse.redirect(loginUrl);
    }
  }

  // 2. Admin routes → separate permission check
  if (ADMIN_ROUTES.some(p => path.startsWith(p))) {
    const adminToken = req.cookies.get('admin')?.value;
    if (!adminToken) {
      return NextResponse.redirect(new URL('/admin/login', req.url));
    }
  }

  // 3. Response headers — enhance security
  const res = NextResponse.next();
  res.headers.set('X-Frame-Options', 'DENY');
  res.headers.set('X-Content-Type-Options', 'nosniff');
  return res;
}

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

// 📁 app/dashboard/page.tsx — now clean
export default async function Dashboard() {
  // No session validation code — middleware already blocked it
  // If reached, confirmed authenticated user
  const data = await db.dashboard.find();
  return <div>{/* ... */}</div>;
}

// Advantages:
// - Protected policy concentrated in one place
// - New protected page = just add to PROTECTED_ROUTES array
// - Edge Runtime → fast response (blocked before reaching page component)
// - Page component is clean (no authentication logic, only business logic)

💡 💡 5 Practical Middleware Tips

1. Negative lookahead patterns in matcher — expressing 'exclusions'

ts
matcher: ['/((?!_next/static|_next/image|favicon|api).*)']

Prevents middleware from touching frequent static asset requests.

2. Edge Runtime constraints — no heavy database calls
Only fast operations like cookie validation or simple JWT decoding. Save real database queries for page components.

3. Setting headers in a single NextResponse.next() call

ts
const res = NextResponse.next();
res.headers.set('x-foo', 'bar');
return res;

4. Setting response cookies

ts
res.cookies.set('name', 'value', { httpOnly: true, secure: true });

httpOnly + secure by default. SameSite=Lax is also recommended.

5. Debugging — console.log goes to server logs
console.log inside middleware appears in server logs (Vultr/Vercel), not in the browser console.

⚡ Try It Yourself — Middleware Behavior Simulation

Simulate how each request path is handled by middleware.
✏️ JS 코드
📟 Console output
▶ Press the Run button
⚠️ Runs in a browser sandbox — only console.log() is supported; alert/fetch are not.

Check Your Understanding

Which of the following can be configured using matcher in middleware.ts?
💡 `matcher` defines **which routes the middleware applies to**. It is common practice to exclude frequent static asset requests (`_next/static`, `_next/image`, `favicon`) and API requests using a negative lookahead pattern so middleware does not touch them. If you omit matcher, the middleware applies to every route, which has a significant performance impact.
Middleware — Authentication·Redirect·Headers - Next.js