React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

cookiesheadersrequest-contextserver-onlynext-headers

Cookies & Headers

Read and set cookies and headers on the server with the cookies() and headers() APIs from next/headers.

Recipe

Quick-reference recipe card -- copy-paste ready.

import { cookies, headers } from "next/headers";
 
// Reading cookies (async in Next.js 15+)
export default async function Page() {
  const cookieStore = await cookies();
  const theme = cookieStore.get("theme")?.value ?? "light";
  const token = cookieStore.get("auth-token")?.value;
 
  // Reading headers
  const headersList = await headers();
  const userAgent = headersList.get("user-agent") ?? "";
  const ip = headersList.get("x-forwarded-for") ?? "unknown";
 
  return <div>Theme: {theme}</div>;
}
 
// Setting cookies in a Server Action
"use server";
import { cookies } from "next/headers";
 
export async function setTheme(theme: string) {
  const cookieStore = await cookies();
  cookieStore.set("theme", theme, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    maxAge: 60 * 60 * 24 * 365, // 1 year
  });
}

When to reach for this: You need to read authentication tokens, locale preferences, feature flags, or any other per-request data from cookies or headers.

Working Example

// app/layout.tsx (Server Component)
import { cookies, headers } from "next/headers";
 
export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const cookieStore = await cookies();
  const theme = cookieStore.get("theme")?.value ?? "system";
  const locale = cookieStore.get("locale")?.value ?? "en";
 
  const headersList = await headers();
  const acceptLanguage = headersList.get("accept-language");
 
  return (
    <html lang={locale} data-theme={theme}>
      <body>{children}</body>
    </html>
  );
}
// app/actions/preferences.ts
"use server";
 
import { cookies } from "next/headers";
import { revalidatePath } from "next/cache";
 
export async function setTheme(formData: FormData) {
  const theme = formData.get("theme") as string;
 
  if (!["light", "dark", "system"].includes(theme)) {
    return { error: "Invalid theme" };
  }
 
  const cookieStore = await cookies();
  cookieStore.set("theme", theme, {
    httpOnly: false, // allow client JS to read for immediate UI update
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    path: "/",
    maxAge: 60 * 60 * 24 * 365,
  });
 
  revalidatePath("/", "layout");
}
 
export async function setLocale(locale: string) {
  const cookieStore = await cookies();
  cookieStore.set("locale", locale, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    path: "/",
    maxAge: 60 * 60 * 24 * 365,
  });
 
  revalidatePath("/", "layout");
}
// app/components/theme-switcher.tsx
"use client";
 
import { useTransition } from "react";
import { setTheme } from "@/app/actions/preferences";
 
export function ThemeSwitcher({ currentTheme }: { currentTheme: string }) {
  const [isPending, startTransition] = useTransition();
 
  const themes = ["light", "dark", "system"] as const;
 
  return (
    <div className="flex gap-2">
      {themes.map((theme) => (
        <button
          key={theme}
          onClick={() =>
            startTransition(async () => {
              const fd = new FormData();
              fd.set("theme", theme);
              await setTheme(fd);
            })
          }
          className={`px-3 py-1 rounded border ${
            currentTheme === theme
              ? "bg-blue-600 text-white"
              : "bg-white text-gray-700"
          } ${isPending ? "opacity-50" : ""}`}
          disabled={isPending}
        >
          {theme}
        </button>
      ))}
    </div>
  );
}
// middleware.ts -- reading and setting cookies/headers in Middleware
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
export function middleware(request: NextRequest) {
  // Read a cookie
  const locale = request.cookies.get("locale")?.value ?? "en";
 
  // Read a header
  const country = request.headers.get("x-vercel-ip-country") ?? "US";
 
  // Set a header for downstream use
  const response = NextResponse.next();
  response.headers.set("x-locale", locale);
  response.headers.set("x-country", country);
 
  // Set a cookie
  if (!request.cookies.has("visitor-id")) {
    response.cookies.set("visitor-id", crypto.randomUUID(), {
      httpOnly: true,
      secure: true,
      sameSite: "lax",
      maxAge: 60 * 60 * 24 * 365,
    });
  }
 
  return response;
}

What this demonstrates:

  • Reading cookies and headers in Server Components and layouts
  • Setting cookies in Server Actions with proper security options
  • Using Middleware to read/set cookies and headers at the edge
  • A theme switcher pattern combining server cookies with client interactivity

Deep Dive

How It Works

  • cookies() and headers() are async functions in Next.js 15+ that return the cookie store and headers map for the current request.
  • Calling either function opts the route into dynamic rendering -- the page cannot be statically generated because it depends on request-time data.
  • cookies() returns a ReadonlyRequestCookies object when read in Server Components. In Server Actions and Route Handlers, it returns a writable store that supports .set() and .delete().
  • headers() returns a read-only Headers object. You cannot set response headers from a Server Component -- use Middleware or Route Handlers for that.
  • Cookies set in Server Actions are sent back as Set-Cookie response headers. The browser applies them immediately.
  • Middleware runs before all route handling and can modify both request and response cookies/headers.

Variations

Deleting a cookie:

"use server";
import { cookies } from "next/headers";
 
export async function logout() {
  const cookieStore = await cookies();
  cookieStore.delete("auth-token");
  cookieStore.delete("session");
}

Reading all cookies:

const cookieStore = await cookies();
const allCookies = cookieStore.getAll();
// [{ name: "theme", value: "dark" }, { name: "locale", value: "en" }]

Checking if a cookie exists:

const cookieStore = await cookies();
const hasAuth = cookieStore.has("auth-token");

Setting response headers in a Route Handler:

// app/api/data/route.ts
import { NextResponse } from "next/server";
 
export async function GET() {
  const data = await fetchData();
 
  return NextResponse.json(data, {
    headers: {
      "Cache-Control": "public, max-age=3600",
      "X-Custom-Header": "my-value",
    },
  });
}

TypeScript Notes

import { cookies, headers } from "next/headers";
 
// cookies() returns Promise<ReadonlyRequestCookies> in Server Components
// and Promise<RequestCookies> in Server Actions (writable)
const cookieStore = await cookies();
const value: string | undefined = cookieStore.get("key")?.value;
 
// headers() returns Promise<ReadonlyHeaders>
const headersList = await headers();
const value: string | null = headersList.get("x-custom");
 
// Cookie options type
type CookieOptions = {
  name: string;
  value: string;
  domain?: string;
  path?: string;
  maxAge?: number;
  expires?: Date;
  httpOnly?: boolean;
  secure?: boolean;
  sameSite?: "strict" | "lax" | "none";
};

Gotchas

  • cookies() and headers() make the route dynamic -- Any Server Component or layout that calls these functions cannot be statically generated. Fix: If you need the cookie value only for interactivity, read it on the client with document.cookie or a library like js-cookie instead.

  • Cannot set cookies in Server Components -- The cookie store is read-only outside of Server Actions and Route Handlers. Fix: Use a Server Action to set cookies, or set them in Middleware.

  • cookies() is async in Next.js 15+ -- Calling cookies() without await returns a Promise, not the cookie store. Fix: Always const cookieStore = await cookies().

  • Middleware cookies vs Server Component cookies -- Cookies set in Middleware via response.cookies.set() are available to downstream Server Components via cookies() in the same request. However, cookies set in Server Actions are only available on the next request. Fix: Understand the request lifecycle; use Middleware for per-request cookie injection.

  • sameSite: "none" requires secure: true -- Browsers reject SameSite=None cookies without the Secure flag. Fix: Always pair sameSite: "none" with secure: true.

  • Cookie size limits -- Browsers limit individual cookies to approximately 4 KB and total cookies per domain to roughly 80. Fix: Store minimal data in cookies; use a session ID pointing to server-side storage for large payloads.

Alternatives

AlternativeUse WhenDon't Use When
Middleware cookiesYou need to read or set cookies before route handlingYou only need cookies inside a Server Action
document.cookie (client)You need to read a non-httpOnly cookie for immediate UI updatesYou need server-side access or httpOnly cookies
js-cookie libraryYou want a friendlier API for client-side cookie managementYou are working in Server Components
Session storage (e.g., iron-session)You need encrypted, tamper-proof session dataSimple key-value preferences suffice
searchParamsData should be visible in the URL and shareableData is sensitive or user-specific

FAQs

Why does calling cookies() or headers() make a route dynamic?
  • These functions depend on request-time data that is different for every visitor
  • The page cannot be statically generated because the cookie/header values are unknown at build time
  • If you only need a cookie for interactivity, read it on the client with document.cookie instead
Why must cookies() and headers() be awaited in Next.js 15+?
  • In Next.js 15+, both are async functions returning Promises
  • Calling cookies() without await gives you a Promise object, not the cookie store
  • Always use const cookieStore = await cookies()
Can you set cookies inside a Server Component?
  • No. The cookie store is read-only in Server Components and layouts
  • Use a Server Action or Route Handler to set cookies
  • Alternatively, set cookies in Middleware
What is the difference between cookies set in Middleware vs Server Actions?
  • Cookies set in Middleware are available to downstream Server Components in the same request
  • Cookies set in Server Actions are sent as Set-Cookie headers and only available on the next request
  • Use Middleware for per-request cookie injection
How do you delete a cookie in a Server Action?
"use server";
import { cookies } from "next/headers";
 
export async function logout() {
  const cookieStore = await cookies();
  cookieStore.delete("auth-token");
  cookieStore.delete("session");
}
What happens if you use sameSite: "none" without secure: true?
  • Browsers reject the cookie entirely
  • SameSite=None requires the Secure flag to be set
  • Always pair sameSite: "none" with secure: true
How do you set custom response headers in Next.js?
  • You cannot set response headers from a Server Component
  • Use a Route Handler with NextResponse.json(data, { headers: {...} })
  • Or use Middleware with response.headers.set("key", "value")
What is the TypeScript type difference between cookies in Server Components vs Server Actions?
// Server Component: ReadonlyRequestCookies (read-only)
const cookieStore = await cookies();
cookieStore.get("key"); // OK
// cookieStore.set(...) // Error
 
// Server Action: RequestCookies (writable)
const cookieStore = await cookies();
cookieStore.set("key", "value", { httpOnly: true }); // OK
What are the browser limits on cookies?
  • Individual cookies are limited to approximately 4 KB
  • Total cookies per domain are roughly limited to 80
  • Store minimal data in cookies; use a session ID pointing to server-side storage for large payloads
How do you type the cookie options object in TypeScript?
type CookieOptions = {
  name: string;
  value: string;
  domain?: string;
  path?: string;
  maxAge?: number;
  expires?: Date;
  httpOnly?: boolean;
  secure?: boolean;
  sameSite?: "strict" | "lax" | "none";
};
How do you read all cookies at once?
const cookieStore = await cookies();
const allCookies = cookieStore.getAll();
// [{ name: "theme", value: "dark" }, { name: "locale", value: "en" }]
Why should you set httpOnly: false for a theme cookie but httpOnly: true for an auth token?
  • httpOnly: false allows client JavaScript to read the cookie for immediate UI updates (e.g., theme)
  • httpOnly: true prevents JavaScript access, protecting sensitive tokens from XSS attacks
  • Always use httpOnly: true for authentication and session cookies