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:
| Option | Effect |
|---|---|
httpOnly: true | JavaScript in the page cannot read it; a session cookie should always have this |
secure: true | sent over HTTPS only |
sameSite: 'Lax' or 'Strict' | not sent on cross-site requests, which is most of the defence against forged requests |
maxAge: seconds | how 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:
POST /loginreads{ "user" }from the body and remembers it in asessioncookie that ishttpOnly, on path/, withsameSiteLax. It answers{ "ok": true, "user" }.GET /meanswers{ "user" }from the session cookie, or401with{ "error": "Not signed in" }.POST /logoutdeletes the session cookie and answers204.PUT /themereads{ "theme" }and remembers it in athemecookie on path/that lasts a year.GET /themeanswers{ "theme" }from the cookie, or"light".GET /cookiesanswers every cookie the request carried.
When it fails
GET /meis401after signing in:setCookiewas 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 listsession.GET /meis still200after logging out:deleteCookiewas given a differentpaththansetCookie, so the browser (and the jar) kept the original. Use the same path.GET /themeforgets the theme after logout: the logout deletedthemetoo, or the theme was stored in the session cookie. They are separate cookies with separate lifetimes.GET /cookiesanswers{}with cookies present: the route answered a literal instead ofgetCookie(c).
Remember
setCookie(c, name, value, options)writesSet-Cookie;getCookie(c, name)reads,getCookie(c)reads all;deleteCookieexpires withMax-Age=0and needs the same path.- The attributes are the security:
httpOnly,secure,sameSitefor sessions;maxAgefor 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.