The contextBasics · Hono

Read what a request carries, its query string, its headers and its JSON body, and shape the response: a status, a header, text or JSON depending on what the client accepts.

What you will learn

Read the theory for The context

All Basics lessons

All Hono courses

loading types…

What you'll learn

  • Read the query string with c.req.query() and a header with c.req.header()
  • Parse a JSON body with await c.req.json<T>() in an async handler
  • Set a status with the second argument of c.json() and a response header with c.header()

The context

A route decides which handler runs. The handler then has to read what the client sent, a filter in the query string, a header saying what format it wants, a JSON body describing a new cat, and shape what goes back, which is more than a body: a status that says what happened, a header that says where the new thing lives. All of that goes through the one object every handler receives, the context c. Its request side is c.req; its response side is the set of helpers on c itself.

The request side: c.req

c.req is Hono's HonoRequest, a thin layer over the standard Request (which is still there as c.req.raw). It reads the parts a handler needs by name:

CallGives you
c.req.param('id')one path parameter, as a string
c.req.query('page')one query value, ?page=2, or undefined
c.req.queries('tag')every value of a repeated key, ?tag=a&tag=b, as an array
c.req.header('accept')one request header, or undefined; the name is case-insensitive
await c.req.json()the body parsed as JSON
await c.req.text()the body as a string
await c.req.parseBody()a form body, application/x-www-form-urlencoded or multipart/form-data
c.req.method, c.req.path, c.req.urlthe verb, the path, the full URL

The body readers return promises, so a handler that uses one is async:

app.post('/notes', async (c) => {
  const body = await c.req.json<{ text: string }>();
  return c.json({ saved: body.text }, 201);
});

The type argument tells TypeScript what shape to expect; it does not check the body. A body that is not valid JSON makes c.req.json() throw, which Hono turns into a 500 unless something catches it. Lesson 5 puts a validator in front of the handler so bad bodies become a 400 with a message; lesson 6 shapes what a thrown error answers.

Query values and headers are string | undefined, and TypeScript holds you to it: Number(c.req.query('minAge')) compiles, but a comparison that assumes the value exists does not. Decide what an absent value means first, usually "no filter", then convert.

The response side

The helpers on c each build a Response with a content type:

c.text('plain text');                  // text/plain; charset=UTF-8
c.json({ ok: true });                  // application/json
c.html('<h1>hi</h1>');                 // text/html
c.body(bytesOrNull, 204);              // whatever you give it, or nothing
c.redirect('/elsewhere');              // 302 with a Location header
c.notFound();                          // whatever app.notFound() answers, 404 Not Found by default

The status is the second argument, c.json(cat, 201), and the type of that argument rules out combinations that make no sense: a body with 204, for instance. Headers are set before the helper runs:

app.get('/report', (c) => {
  c.header('x-generated-at', new Date().toISOString());
  c.header('cache-control', 'no-store');
  return c.json({ rows: [] });
});

c.header(name, value) remembers the header and the next helper puts it on the response it builds. c.status(201) does the same for the status, for the times the status is decided before the body is; the second argument of the helper is the shorter form when both are known at once.

Answering in the format the client asked for

A request's Accept header says what the client can read. A handler that honours it reads the header and picks a helper:

app.get('/time', (c) => {
  const now = new Date();
  if (c.req.header('accept')?.includes('text/plain')) return c.text(now.toISOString());
  return c.json({ now: now.toISOString() });
});

The ?. is there because the header may be absent, and includes rather than === because a real Accept is a list such as text/plain, */*;q=0.8.

Your task

The app has the list and the lookup. Grow it on the same cats array:

  1. GET /cats honours ?minAge=<n>, answering only the cats at least that old, and always sets an x-total-count header to the number of cats it answers.
  2. GET /cats/:id answers the cat's name as plain text when the request's Accept header asks for text/plain, and the cat as JSON otherwise.
  3. POST /cats reads a JSON body with name and age, stores the cat under the next id, and answers 201 with the cat and a Location header of /cats/<id>.
  4. PATCH /cats/:id merges the JSON body's fields into the stored cat and answers the updated cat, or the usual 404 with { "error": "Cat <id> not found" }.

When it fails

  • POST /cats answers 200: the status was not given. c.json(cat, 201).
  • The Location or x-total-count header is missing: c.header() was called after return, where it never runs, or not at all. Set headers before returning the helper's result.
  • GET /cats?minAge=3 answers every cat: the filter compared cat.age >= minAge with the string '3'. It happens to work for single digits and fails for '10'; convert with Number().
  • Cannot read properties of undefined in the console on PATCH: the merge ran on a cat that was not found. Answer the 404 first, then merge.
  • await in a handler that is not async is a type error the editor shows before Run. Add async.

Remember

  • c.req reads the request: param(), query(), header(), json(); the body readers are async.
  • c.text(), c.json(), c.html(), c.body(), c.redirect() build the response; the status is their second argument.
  • c.header(name, value) before the helper puts a header on the response.
  • Query values and headers may be absent; decide what absent means before converting.
Stuck? Show a hint

c.req.query('minAge') is undefined when the query string has no such key, and a string otherwise. Headers set with c.header(name, value) before returning end up on the response. A handler that awaits c.req.json() must be async. The next id is one more than the number of cats, since nothing is removed in this lesson.