Cookies & Headers
Read and set cookies and headers on the server with the cookies() and headers() APIs from next/headers.
Search across all documentation pages
Read and set cookies and headers on the server with the cookies() and headers() APIs from next/headers.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
// 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:
cookies() and headers() are async functions in Next.js 15+ that return the cookie store and headers map for the current request.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.Set-Cookie response headers. The browser applies them immediately.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",
},
});
}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";
};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.
| Alternative | Use When | Don't Use When |
|---|---|---|
| Middleware cookies | You need to read or set cookies before route handling | You only need cookies inside a Server Action |
document.cookie (client) | You need to read a non-httpOnly cookie for immediate UI updates | You need server-side access or httpOnly cookies |
js-cookie library | You want a friendlier API for client-side cookie management | You are working in Server Components |
| Session storage (e.g., iron-session) | You need encrypted, tamper-proof session data | Simple key-value preferences suffice |
searchParams | Data should be visible in the URL and shareable | Data is sensitive or user-specific |
document.cookie insteadcookies() without await gives you a Promise object, not the cookie storeconst cookieStore = await cookies()Set-Cookie headers and only available on the next request"use server";
import { cookies } from "next/headers";
export async function logout() {
const cookieStore = await cookies();
cookieStore.delete("auth-token");
cookieStore.delete("session");
}SameSite=None requires the Secure flag to be setsameSite: "none" with secure: trueNextResponse.json(data, { headers: {...} })response.headers.set("key", "value")// 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 }); // OKtype CookieOptions = {
name: string;
value: string;
domain?: string;
path?: string;
maxAge?: number;
expires?: Date;
httpOnly?: boolean;
secure?: boolean;
sameSite?: "strict" | "lax" | "none";
};const cookieStore = await cookies();
const allCookies = cookieStore.getAll();
// [{ name: "theme", value: "dark" }, { name: "locale", value: "en" }]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 attackshttpOnly: true for authentication and session cookiesReviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥