Cookies for Frontend Engineers
document.cookie, HttpOnly, Secure, SameSite, and when cookies beat localStorage for auth — practical rules without backend myths.
- javascript
- cookies
- auth
- security
Cookies are small key/value pairs the browser stores per domain and automatically sends on matching requests. That auto-send is why they still win for session auth — and why misconfigured cookies become CSRF and XSS disaster fuel.
Frontend engineers don’t set server cookie policy alone, but you must read document.cookie, know what JS cannot see, and pick SameSite correctly when you own the client.
document.cookie is a weird string
// read all non-HttpOnly cookies for this document
console.log(document.cookie);
// "theme=dark; locale=en"
// write (one cookie per assignment)
document.cookie = `theme=dark; path=/; max-age=${60 * 60 * 24 * 365}; samesite=lax`;
// delete: expire in the past
document.cookie = 'theme=; path=/; max-age=0';
There is no structured API in the platform for listing cookies with attributes. You parse the string yourself:
function getCookie(name) {
const prefix = `${encodeURIComponent(name)}=`;
return document.cookie
.split('; ')
.find((row) => row.startsWith(prefix))
?.slice(prefix.length);
}
Attributes that matter
| Attribute | Why you care |
|---|---|
HttpOnly |
JS cannot read — mitigates token theft via XSS |
Secure |
Only sent over HTTPS |
SameSite=Lax/Strict/None |
Cross-site send rules (CSRF) |
Path / Domain |
Scope of where it’s sent |
Max-Age / Expires |
Lifetime |
Partitioned |
CHIPS — third-party cookie partitioning |
// client-set cookie for UI prefs only — never session secrets
document.cookie =
'sidebar=collapsed; path=/; max-age=31536000; samesite=lax; secure';
Auth tokens in localStorage are readable by any XSS script. HttpOnly cookies are not. That is the usual argument for cookie-based sessions — paired with CSRF defenses.
SameSite in practice
- Strict — never sent on cross-site navigations (can break OAuth return links).
- Lax — sent on top-level GET navigations; good default for many session cookies.
- None — cross-site; requires Secure. Needed for some embeds/third-party flows.
Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Lax; Path=/
What frontend can and cannot do
// can: theme, locale, non-sensitive UI state
// cannot: read HttpOnly session cookie
// should not: store long-lived access tokens in JS-visible cookies without threat modeling
fetch / XHR send same-origin cookies by default. Cross-origin needs credentials: 'include' and server Access-Control-Allow-Credentials + explicit origin (not *).
await fetch('https://api.example.com/me', {
credentials: 'include',
});
Size and count limits
Browsers roughly allow ~4KB per cookie and a few hundred per domain. Don’t stuff JWTs with full profile graphs into cookies — keep sessions server-side or use short opaque IDs.
Interview answer
“Cookies are sent automatically on matching requests. document.cookie only sees non-HttpOnly cookies and uses a string API. For auth I prefer HttpOnly+Secure+SameSite session cookies set by the server, with CSRF strategy as needed. localStorage is fine for non-secrets; XSS can steal anything JS can read.”
Related
Prefixes and third-party sunset
Set-Cookie: __Host-session=abc; Path=/; Secure; HttpOnly; SameSite=Lax
__Host- requires Secure, no Domain, Path=/ — hardest to mis-scope. __Secure- requires Secure. Prefer these prefixes for session cookies when you control the server.
Third-party cookies are being retired in major browsers. Embeds that relied on third-party session cookies need Storage Access API, first-party proxies, or partitioned cookies (Partitioned). As a frontend engineer, don’t design new features that require third-party cookie reads from JS (document.cookie never saw HttpOnly ones anyway, and cross-site readable cookies are increasingly blocked).
Further reading
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.