document.cookie is one of the web's more awkward APIs: reading it gives you a single string of "a=1; b=2" pairs, while writing it takes one "key=value; attr=…" string at a time (assigning doesn't replace everything — it adds or updates just that cookie). And values must be URL-encoded, since a raw ; or = would corrupt the string. A tiny cookieStore wrapper hides all of that behind get/set/remove/getAll.
Implement cookieStore(). Return { get, set, remove, getAll } that encode/decode values, serialize attributes (path, max-age), and delete cookies by expiring them.
function cookieStore() {
// returns { get(name), set(name, value, options), remove(name, options), getAll() }
}
const cookies = cookieStore();
cookies.set('theme', 'dark');
cookies.get('theme'); // 'dark'
cookies.set('token', 'a=b; c', { maxAge: 3600 }); // value is encoded
cookies.getAll(); // { theme: 'dark', token: 'a=b; c' }
cookies.remove('theme'); // gone
encodeURIComponent the name and value on write, decodeURIComponent on read, so ;/=/spaces survive.document.cookie on '; ', then each pair on its first = (values can contain encoded =).document.cookie = 'name=value; path=/; max-age=…' adds/updates that one; it doesn't clear others.max-age=0 (same path) so the browser drops it.You'll wrap document.cookie: set serializes an encoded name=value plus attributes, getAll parses the read string into a decoded object, get reads one key from it, and remove re-sets the cookie with max-age=0.
document.cookie is asymmetric. Reading returns all cookies as "a=1; b=2". Writing takes one cookie's serialized form and merges it in. So the wrapper has two halves: a serializer (set/remove) that builds name=value; path=…; max-age=… strings, and a parser (get/getAll) that splits the read string back into an object. Both halves must URL-encode/decode, because a value like a=b; c contains the very delimiters cookies use — leave it raw and you'd write a broken, ambiguous string. And there's no "delete cookie" call: you remove one by setting it with an already-elapsed lifetime (max-age=0).
Two directions:
set(name, value, opts) → encodeURIComponent both parts, append ; path=… and ; max-age=…, assign to document.cookie. remove is a set with max-age=0.getAll() → split document.cookie on '; ', split each pair on its first =, decodeURIComponent both sides into an object. get(name) is a lookup in that object.Encoding is the thread through both: encode on the way out, decode on the way in, so arbitrary values round-trip.
The naive version skips encoding and parses too eagerly:
function cookieStoreNaive() {
return {
set: (name, value) => { document.cookie = `${name}=${value}`; }, // no encode, no path
get: (name) => {
const pairs = document.cookie.split(';'); // no space handling
for (const p of pairs) {
const [k, v] = p.split('='); // breaks on '=' in value
if (k === name) return v; // stray whitespace in k
}
return null;
},
};
}
Bugs everywhere. Setting a value containing ; or = (a JWT, a JSON blob) corrupts the cookie string. split('=') breaks any value that itself contains = (base64 padding!). split(';') without trimming leaves leading spaces on names ( b vs b). And there's no max-age/path, so remove is impossible. Encoding, splitting on the first =, and trimming fix it.
function cookieStore() {
function set(name, value, options = {}) {
const { maxAge, path = '/' } = options;
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
cookie += `; path=${path}`;
if (maxAge !== undefined) cookie += `; max-age=${maxAge}`;
document.cookie = cookie;
}
function getAll() {
const result = {};
if (!document.cookie) return result;
for (const pair of document.cookie.split('; ')) {
const eq = pair.indexOf('='); // split on the FIRST '=' only
if (eq === -1) continue;
const name = decodeURIComponent(pair.slice(0, eq));
const value = decodeURIComponent(pair.slice(eq + 1));
result[name] = value;
}
return result;
}
function get(name) {
const all = getAll();
return name in all ? all[name] : null;
}
function remove(name, options = {}) {
const { path = '/' } = options;
// Expire it: same name/path, zero lifetime.
document.cookie = `${encodeURIComponent(name)}=; path=${path}; max-age=0`;
}
return { get, set, remove, getAll };
}
module.exports = { cookieStore };
set encodes both name and value (so delimiters survive), always writes path=/ (so remove can target the same cookie), and appends max-age when given. getAll parses document.cookie: split on '; ', then split each pair on the first = via indexOf/slice (a value can contain encoded/real =), decoding both sides into an object. get is a lookup with an explicit null for absence. remove re-sets the cookie with max-age=0 and the same path, which the browser treats as "expired now" and drops. Encoding on write + decoding on read makes any value round-trip.
store.set('token', 'a=b; c') then store.get('token'), then store.remove('token'):
encodeURIComponent('a=b; c') → 'a%3Db%3B%20c'. Writes token=a%3Db%3B%20c; path=/. The ;/=/space are now safely %3B/%3D/%20, so the cookie string isn't corrupted.getAll splits document.cookie on '; ', finds token=a%3Db%3B%20c, splits on the first = → name token, value a%3Db%3B%20c; decodeURIComponent → 'a=b; c'. The exact original value came back.token=; path=/; max-age=0. The browser expires it immediately; a subsequent get('token') returns null.;/=/space corrupts the cookie string (or splits into phantom cookies). encodeURIComponent both name and value.split('=') on a pair — breaks values containing = (base64 ==, a=b). Split on the first = with indexOf/slice.split(';') — names get a leading space. Split on '; ' (or .trim()).max-age=0 (and the same path, or the browser won't match the cookie you meant).SameSite / Secure / HttpOnly — real cookies carry security attributes; HttpOnly cookies are invisible to document.cookie entirely (that's the point).JSON.stringify + a signature over set/get gives typed, tamper-evident cookies.cookieStore.get()/set() (a real browser API) supersedes document.cookie; this exercise reimplements its ergonomics over the legacy string.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
document.cookie is one of the web's more awkward APIs: reading it gives you a single string of "a=1; b=2" pairs, while writing it takes one "key=value; attr=…" string at a time (assigning doesn't replace everything — it adds or updates just that cookie). And values must be URL-encoded, since a raw ; or = would corrupt the string. A tiny cookieStore wrapper hides all of that behind get/set/remove/getAll.
Implement cookieStore(). Return { get, set, remove, getAll } that encode/decode values, serialize attributes (path, max-age), and delete cookies by expiring them.
function cookieStore() {
// returns { get(name), set(name, value, options), remove(name, options), getAll() }
}
const cookies = cookieStore();
cookies.set('theme', 'dark');
cookies.get('theme'); // 'dark'
cookies.set('token', 'a=b; c', { maxAge: 3600 }); // value is encoded
cookies.getAll(); // { theme: 'dark', token: 'a=b; c' }
cookies.remove('theme'); // gone
encodeURIComponent the name and value on write, decodeURIComponent on read, so ;/=/spaces survive.document.cookie on '; ', then each pair on its first = (values can contain encoded =).document.cookie = 'name=value; path=/; max-age=…' adds/updates that one; it doesn't clear others.max-age=0 (same path) so the browser drops it.You'll wrap document.cookie: set serializes an encoded name=value plus attributes, getAll parses the read string into a decoded object, get reads one key from it, and remove re-sets the cookie with max-age=0.
document.cookie is asymmetric. Reading returns all cookies as "a=1; b=2". Writing takes one cookie's serialized form and merges it in. So the wrapper has two halves: a serializer (set/remove) that builds name=value; path=…; max-age=… strings, and a parser (get/getAll) that splits the read string back into an object. Both halves must URL-encode/decode, because a value like a=b; c contains the very delimiters cookies use — leave it raw and you'd write a broken, ambiguous string. And there's no "delete cookie" call: you remove one by setting it with an already-elapsed lifetime (max-age=0).
Two directions:
set(name, value, opts) → encodeURIComponent both parts, append ; path=… and ; max-age=…, assign to document.cookie. remove is a set with max-age=0.getAll() → split document.cookie on '; ', split each pair on its first =, decodeURIComponent both sides into an object. get(name) is a lookup in that object.Encoding is the thread through both: encode on the way out, decode on the way in, so arbitrary values round-trip.
The naive version skips encoding and parses too eagerly:
function cookieStoreNaive() {
return {
set: (name, value) => { document.cookie = `${name}=${value}`; }, // no encode, no path
get: (name) => {
const pairs = document.cookie.split(';'); // no space handling
for (const p of pairs) {
const [k, v] = p.split('='); // breaks on '=' in value
if (k === name) return v; // stray whitespace in k
}
return null;
},
};
}
Bugs everywhere. Setting a value containing ; or = (a JWT, a JSON blob) corrupts the cookie string. split('=') breaks any value that itself contains = (base64 padding!). split(';') without trimming leaves leading spaces on names ( b vs b). And there's no max-age/path, so remove is impossible. Encoding, splitting on the first =, and trimming fix it.
function cookieStore() {
function set(name, value, options = {}) {
const { maxAge, path = '/' } = options;
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
cookie += `; path=${path}`;
if (maxAge !== undefined) cookie += `; max-age=${maxAge}`;
document.cookie = cookie;
}
function getAll() {
const result = {};
if (!document.cookie) return result;
for (const pair of document.cookie.split('; ')) {
const eq = pair.indexOf('='); // split on the FIRST '=' only
if (eq === -1) continue;
const name = decodeURIComponent(pair.slice(0, eq));
const value = decodeURIComponent(pair.slice(eq + 1));
result[name] = value;
}
return result;
}
function get(name) {
const all = getAll();
return name in all ? all[name] : null;
}
function remove(name, options = {}) {
const { path = '/' } = options;
// Expire it: same name/path, zero lifetime.
document.cookie = `${encodeURIComponent(name)}=; path=${path}; max-age=0`;
}
return { get, set, remove, getAll };
}
module.exports = { cookieStore };
set encodes both name and value (so delimiters survive), always writes path=/ (so remove can target the same cookie), and appends max-age when given. getAll parses document.cookie: split on '; ', then split each pair on the first = via indexOf/slice (a value can contain encoded/real =), decoding both sides into an object. get is a lookup with an explicit null for absence. remove re-sets the cookie with max-age=0 and the same path, which the browser treats as "expired now" and drops. Encoding on write + decoding on read makes any value round-trip.
store.set('token', 'a=b; c') then store.get('token'), then store.remove('token'):
encodeURIComponent('a=b; c') → 'a%3Db%3B%20c'. Writes token=a%3Db%3B%20c; path=/. The ;/=/space are now safely %3B/%3D/%20, so the cookie string isn't corrupted.getAll splits document.cookie on '; ', finds token=a%3Db%3B%20c, splits on the first = → name token, value a%3Db%3B%20c; decodeURIComponent → 'a=b; c'. The exact original value came back.token=; path=/; max-age=0. The browser expires it immediately; a subsequent get('token') returns null.;/=/space corrupts the cookie string (or splits into phantom cookies). encodeURIComponent both name and value.split('=') on a pair — breaks values containing = (base64 ==, a=b). Split on the first = with indexOf/slice.split(';') — names get a leading space. Split on '; ' (or .trim()).max-age=0 (and the same path, or the browser won't match the cookie you meant).SameSite / Secure / HttpOnly — real cookies carry security attributes; HttpOnly cookies are invisible to document.cookie entirely (that's the point).JSON.stringify + a signature over set/get gives typed, tamper-evident cookies.cookieStore.get()/set() (a real browser API) supersedes document.cookie; this exercise reimplements its ergonomics over the legacy string.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.