ETags
Tag every cat response with etag() so a client that already has it gets 304 and no body, watch the tag change when the cat changes, and pair it with Cache-Control.
What you'll learn
- Add etag() on a prefix and read the tag it computes from the body
- Send If-None-Match back and get 304 with no body and only the retained headers
- See a change to the resource change its tag, and pair the tag with Cache-Control: max-age
A client that shows a cat's page asks for /cats/1 every time the page opens, and the API serialises and sends the same cat every time. The bytes are small; the habit is not. HTTP has a two-line protocol for "has this changed?": the server tags each response with an identifier of its content, the client sends the tag back on its next request, and the server answers 304 Not Modified with no body when the tag still matches. Hono's etag middleware does the tagging and the comparing, and this lesson adds it to the cats API together with the caching header it works best beside.
The exchange
GET /cats/1
→ 200, ETag: "5096e5…", body
GET /cats/1, If-None-Match: "5096e5…"
→ 304, ETag: "5096e5…", no body
PATCH /cats/1 …
GET /cats/1, If-None-Match: "5096e5…"
→ 200, ETag: "a1d3…", body
The tag is opaque to the client; it only has to send it back. If-None-Match may also carry several tags, or *, meaning "if anything exists".
etag()
import { etag } from 'hono/etag';
app.use('/etag/*', etag());
app.get('/etag/abc', (c) => c.text('Hono is hot'));
For a successful GET (or HEAD) the middleware hashes the response body with SHA-1 and sets ETag to the hex digest in quotes, "5096e579189b967623bedf1a4ea178bf19e5086d". When the request's If-None-Match matches, it replaces the response with a 304 that has no body and keeps only the headers a client needs to interpret it: ETag, Cache-Control, Content-Location, Date, Expires and Vary; the retainedHeaders option changes that list. A handler that sets its own ETag keeps it, and the middleware only compares. weak: true emits W/"…", a weak tag, for responses that are equivalent rather than identical byte for byte.
Note what the middleware cannot save. The handler still runs and still builds the body, since the hash is computed from it; what a 304 saves is the transfer, not the work. An API whose responses are expensive to compute needs a cache in front of the handler, which is a different tool.
Cache-Control beside it
An ETag alone makes every request a round trip that may end in 304. Cache-Control: max-age=60 tells the client it may reuse the response for a minute without asking at all, and after that minute the If-None-Match round trip resumes. The two headers answer two questions, "how long may I not ask?" and "when I do ask, has it changed?", and a response worth caching usually carries both. Because Cache-Control is one of the retained headers, the 304 carries it too, restarting the client's minute.
Where it sits
Above the routes it should tag, on a prefix. The middleware acts after next(), on the response, so its position relative to guards does not matter, and it ignores errors and other statuses: a 404 gets no tag.
Your task
The cat routes answer full bodies every time.
- Tag every response under
/catswithetag(), so a request whoseIf-None-Matchmatches is answered304. GET /cats/:idsetsCache-Control: max-age=60.
Send GET /cats/1 from the request bar, copy the ETag from the response headers into an If-None-Match request header, and send again; then PATCH /cats/1 and send the conditional request once more.
When it fails
- No
etagheader onGET /cats/1: the middleware is on the wrong path, or registered below the route. - The conditional request answers
200with the same tag:If-None-Matchwas sent without the quotes. The tag includes them. - The
304lackscache-control: the handler never set it. Only headers that exist can be retained. - After the
PATCHthe old tag still gets304: the handler answered a copy made before the change, or thePATCHdid not modify the stored cat.
Remember
etag()tags successfulGETresponses with a quoted SHA-1 of the body and answers a matchingIf-None-Matchwith304and no body.- The
304keepsETag,Cache-Control,Content-Location,Date,ExpiresandVary;retainedHeaderschanges the list. - A
304saves the transfer, not the handler's work. - Pair it with
Cache-Control: max-agefor the "do not even ask" period.
Stuck? Show a hint
app.use('/cats/*', etag()) tags every successful GET under /cats. The handler sets c.header('Cache-Control', 'max-age=60') before c.json(); the middleware keeps that header on the 304. Nothing about the PATCH changes.