InteractiveFrameworks

Cookies

Keep state across requests the way a browser does: a session cookie set at login and read on every request, a preference cookie that outlives the session, and a logout that deletes one without touching the other.

What you'll learn

  • Set a cookie with setCookie() from hono/cookie and choose its attributes: httpOnly, path, sameSite, maxAge
  • Read one cookie with getCookie(c, name) and all of them with getCookie(c)
  • Delete a cookie with deleteCookie(), which is a Set-Cookie that expires it, and see the jar in the request panel follow

Every request the cats API has answered so far stood alone: nothing about one request was known to the next. A real client wants to sign in once and be recognised afterwards, or pick a theme and keep it. HTTP has one mechanism for that which needs nothing from the client but a browser: the server answers with a Set-Cookie header, the browser stores the cookie and sends it back, as a Cookie header, with every later request to that site. Hono's hono/cookie helper reads and writes those headers; what to put in them is the design decision this lesson is about.

Setting, reading, deleting

import { getCookie, setCookie, deleteCookie } from 'hono/cookie';

app.post('/sign-in', (c) => {
  setCookie(c, 'session', 'user-42', { httpOnly: true, path: '/', sameSite: 'Lax' });
  return c.json({ ok: true });
});

app.get('/profile', (c) => {
  const session = getCookie(c, 'session');
  if (!session) return c.json({ error: 'Not signed in' }, 401);
  return c.json({ session });
});

app.post('/sign-out', (c) => {
  deleteCookie(c, 'session', { path: '/' });
  return c.body(null, 204);
});

setCookie(c, name, value, options) appends a Set-Cookie header to the response, session=user-42; Path=/; HttpOnly; SameSite=Lax for the call above. getCookie(c, name) reads one cookie from the request's Cookie header and gives undefined when it is not there; getCookie(c) with no name gives every cookie as an object. A cookie is a string on both sides, so anything structured goes through JSON.stringify and back, and anything the client must not tamper with goes through a signature.

There is no "delete" in the protocol. deleteCookie sends a Set-Cookie for the same name with Max-Age=0, session=; Max-Age=0; Path=/, which tells the browser to drop it. It has to name the same path (and domain) the cookie was set with, or the browser sees a different cookie and keeps the original.

The attributes are the security

The options decide who can read the cookie and when it is sent:

OptionEffect
httpOnly: trueJavaScript in the page cannot read it; a session cookie should always have this
secure: truesent over HTTPS only
sameSite: 'Lax' or 'Strict'not sent on cross-site requests, which is most of the defence against forged requests
maxAge: secondshow long the browser keeps it; without it, the cookie dies with the browser session
path: '/'the paths it is sent to; Hono defaults it to /

A session cookie is httpOnly, secure in production, sameSite at least Lax, and has no maxAge or a short one. A preference the page itself may read has no httpOnly and a long maxAge. The helper refuses a maxAge over 400 days, following the current cookie standard.

Signed cookies

A cookie is the client's to edit. When the value must be trusted, setSignedCookie(c, name, value, secret) appends an HMAC signature and getSignedCookie(c, secret, name) verifies it: a valid cookie gives the value, a tampered one gives false, an absent one undefined. Both are async, because the signature is computed with the Web Crypto API. Hono also understands the __Host- and __Secure- prefixes, which browsers enforce, through the prefix option.

What this tab does with cookies

The Endpoints tab and the request bar keep a cookie jar for the running app, as a browser would: each Set-Cookie the app sends is stored, and every later request carries the matching cookies. The panel lists the jar after each request, so setCookie and deleteCookie can be watched working. The graded endpoints are a sequence for that reason: sign in, then read, then sign out.

Your task

Six routes, one jar:

  1. POST /login reads { "user" } from the body and remembers it in a session cookie that is httpOnly, on path /, with sameSite Lax. It answers { "ok": true, "user" }.
  2. GET /me answers { "user" } from the session cookie, or 401 with { "error": "Not signed in" }.
  3. POST /logout deletes the session cookie and answers 204.
  4. PUT /theme reads { "theme" } and remembers it in a theme cookie on path / that lasts a year.
  5. GET /theme answers { "theme" } from the cookie, or "light".
  6. GET /cookies answers every cookie the request carried.

When it fails

  • GET /me is 401 after signing in: setCookie was never called, or the value was put somewhere else (a module variable is shared by every client, which is not a session). Check the jar in the request panel: it should list session.
  • GET /me is still 200 after logging out: deleteCookie was given a different path than setCookie, so the browser (and the jar) kept the original. Use the same path.
  • GET /theme forgets the theme after logout: the logout deleted theme too, or the theme was stored in the session cookie. They are separate cookies with separate lifetimes.
  • GET /cookies answers {} with cookies present: the route answered a literal instead of getCookie(c).

Remember

  • setCookie(c, name, value, options) writes Set-Cookie; getCookie(c, name) reads, getCookie(c) reads all; deleteCookie expires with Max-Age=0 and needs the same path.
  • The attributes are the security: httpOnly, secure, sameSite for sessions; maxAge for how long.
  • A cookie is the client's to edit; sign what must be trusted.
  • The request panel's jar is the browser's cookie store for this run.
Stuck? Show a hint

setCookie(c, name, value, options) adds a Set-Cookie header; the next request the panel sends carries the cookie back, and getCookie(c, name) reads it, or undefined when there is none. deleteCookie(c, name, { path: '/' }) needs the same path the cookie was set with. A cookie for a year is maxAge: 60 * 60 * 24 * 365.