C
Next.js/API/Lesson 13

next/headers — cookies / headers / redirect / notFound Helpers

25 min·theory
This chapter
2/2
TypeScript

next/headers — cookies / headers / redirect / notFound Helpers

💡 Why Learn This? — 4 Tools Exclusive to Server Code

🎯 In Server Components, Actions, and Route Handlers, `cookies()` and `headers()` are essential for reading request information (cookies and headers).
💼 To render per-user UI (greetings, language, theme), you need request data on every request — the moment you call these, the page automatically switches to dynamic rendering.
Redirects and 404s can also be handled inside components with a single call to `redirect()` or `notFound()`. This terminates the flow (similar to a throw).
🔗 These calls cannot be made in Client Components — for similar functionality, use browser APIs such as `useRouter` or `document.cookie`.
📈 When these helpers are called, the page automatically transitions from SSG to SSR, which affects your caching strategy.
🏢 실무에서는
Displaying a logged-in user's name, internationalization (Accept-Language header → Korean vs. English), A/B testing (determining group via cookie), and handling 404s on pages with no data — all of these fall within the scope of these four helpers.

4 Helpers — Where to Use Them and Their Signatures

1. cookies() — Read Request Cookies, Write Response Cookies

ts
import { cookies } from 'next/headers';

export default async function Page() {
  const cookieStore = cookies();
  const session = cookieStore.get('session')?.value;
  const all = cookieStore.getAll();
  const has = cookieStore.has('darkMode');
  // ...
}

Reading is available anywhere — Server Components, Actions, and Route Handlers. Writing (set/delete) is only allowed in Server Actions and Route Handlers.

2. headers() — Read Request Headers (Read-Only)

ts
import { headers } from 'next/headers';

export default async function Page() {
  const headersList = headers();
  const userAgent = headersList.get('user-agent');
  const lang = headersList.get('accept-language');
  const ip = headersList.get('x-forwarded-for')?.split(',')[0];
}

Modifying response headers is not possible here (use middleware or NextResponse instead).

3. redirect() — Redirect to Another Path

ts
import { redirect } from 'next/navigation';

export default async function Page() {
  const session = cookies().get('session');
  if (!session) redirect('/login');  // ← terminates flow like a throw
  // never reached
}
  • Code after redirect() does not execute (TypeScript even narrows types accordingly).
  • Available in Server Components, Actions, and Route Handlers.
  • For permanent redirects, use permanentRedirect() (HTTP 308).

4. notFound() — Trigger the 404 Page

ts
import { notFound } from 'next/navigation';

export default async function PostPage({ params }: { params: { id: string } }) {
  const post = await db.post.findUnique({ where: { id: Number(params.id) } });
  if (!post) notFound();  // ← renders not-found.tsx in the same folder
  return <article>{post.title}</article>;
}

If a not-found.tsx exists in the same folder or a nearby ancestor, it will be rendered.

5. Side Effect of Calling These — Page Switches to Dynamic Rendering

ts
// Neither called → SSG (static at build time)
export default function Page() {
  return <h1>Hello</h1>;
}

// cookies() called → SSR on every request (per-user, so static is not possible)
export default function Page() {
  const session = cookies().get('session');
  return <h1>{session ? 'Logged in' : 'Guest'}</h1>;
}

Regardless of the fetch cache mode, the page switches to dynamic rendering. This is the #1 reason a page unexpectedly becomes SSR on every request.

6. Commonly Used Together — A Typical Pattern

ts
import { cookies, headers } from 'next/headers';
import { redirect, notFound } from 'next/navigation';

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

  const user = await db.user.findUnique({
    where: { sessionId: session },
  });
  if (!user) notFound();

  const lang = headers().get('accept-language')?.startsWith('ko') ? 'ko' : 'en';

  return <div>{lang === 'ko' ? `Hi, ${user.name}` : `Hi ${user.name}`}</div>;
}
💻 🅰️ Pages Router — getServerSideProps + ctx
// ❌ Pages Router — using req/res from ctx

// 📁 pages/dashboard.tsx
import type { GetServerSideProps } from 'next';

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  // Read cookie
  const session = ctx.req.cookies.session;
  if (!session) {
    return {
      redirect: { destination: '/login?next=/dashboard', permanent: false },
    };
  }

  // Read header
  const lang = ctx.req.headers['accept-language']?.startsWith('ko') ? 'ko' : 'en';

  // DB query
  const user = await db.user.findUnique({ where: { sessionId: session } });
  if (!user) return { notFound: true };

  return { props: { user, lang } };
};

export default function Dashboard({ user, lang }) {
  return <div>{lang === 'ko' ? `${user.name}` : `Hi ${user.name}`}</div>;
}

// Disadvantages:
// - Data · authentication · render separation
// - Cannot access cookies/headers directly within the component (only via props)
// - Expressing redirect/notFound with return object — imperative flow is unnatural
💻 🅱️ next/headers — Call Directly Inside Components
// ✅ next/headers + next/navigation — directly inside component

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

export default async function Dashboard() {
  // Read cookie
  const session = cookies().get('session')?.value;
  if (!session) redirect('/login?next=/dashboard');
  // ↑ Flow terminates like throw — code below narrows session to string

  // Read header
  const lang = headers().get('accept-language')?.startsWith('ko') ? 'ko' : 'en';

  // DB query + 404
  const user = await db.user.findUnique({ where: { sessionId: session } });
  if (!user) notFound();
  // ↑ user is narrowed to non-null

  return <div>{lang === 'ko' ? `${user.name} Hi` : `Hi ${user.name}`}</div>;
}

// 📁 app/posts/[id]/page.tsx — using 404
import { notFound } from 'next/navigation';

export default async function PostPage({ params }: { params: { id: string } }) {
  const post = await db.post.findUnique({ where: { id: Number(params.id) } });
  if (!post) notFound();
  // ↓ not-found.tsx in the same folder is rendered
  return <article>{post.title}</article>;
}

// 📁 app/api/me/route.ts — Same helper in Route Handler
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';

export async function GET() {
  const session = cookies().get('session')?.value;
  if (!session) return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
  const user = await db.user.findUnique({ where: { sessionId: session } });
  return NextResponse.json(user);
}

// 📁 app/actions/logout.ts — Delete cookie in Server Action
'use server';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

export async function logout() {
  cookies().delete('session');
  redirect('/');
}

// Advantages:
// - Direct call within component — data · authentication · render in one flow
// - TS narrowing: automatic non-null inference for variables after redirect/notFound
// - Route Handler · Server Action also use the same import — consistent API

💡 💡 next/headers in Practice — 5 Tips

1. Calling cookies() switches the page to dynamic rendering

ts
const c = cookies(); // this line overrides force-cache

Pages that read cookies are SSR on every request. Avoid this call if you want SSG.

2. headers() does the same — triggers dynamic rendering
User-agent branching, A/B testing, etc. — the response differs per request, so static rendering is not possible.

3. redirect / notFound behave like throw
Code after the call does not run. TypeScript narrows types inside the if block, guaranteeing variables are non-null.

4. cookies().set/delete only works in Server Actions or Route Handlers
In a Server Component you can only read cookies. Writes must happen in a mutation context.

5. Always use optional chaining: cookies().get('x')?.value
cookies().get('x') returns RequestCookie | undefined. Never access .value directly — use ?.value for safe access.

⚡ Try It Yourself — Authentication Flow Simulation

Simulate the cookies + redirect + notFound flow in a Server Component.
✏️ JS 코드
📟 Console output
▶ Press the Run button
⚠️ Runs in a browser sandbox — only console.log() is supported; alert/fetch are not.

Check Your Understanding

What effect does a single line `const c = cookies();` in a Server Component have on the page?
💡 Calling `cookies()` or `headers()` signals to Next.js that 'this page needs a different response on every request,' causing it to switch the page to **dynamic** rendering. The `force-cache` option on fetch is also ignored, and every request goes through SSR. To maintain static caching (SSG), you should either avoid these calls or isolate the per-user portions into a Client Component.
next/headers — cookies / headers / redirect - Next.js