InteractiveFrameworks

Requests and responses

What travels between a client and your server: a method, a path, a query string, headers and a body one way, and a status, headers and a body the other. Build a server that describes every request it receives.

What you'll learn

  • Name the parts of an HTTP request (method, path, query, headers, body) and of a response (status, headers, body)
  • Read each part of a request in Hono, and tell an absent header from an empty one
  • Set a response's headers and body, and see that a HEAD request gets the headers without the body

Every lesson on this site ends the same way: something sends your server a request, and your server sends back a response. NestJS and Hono are different ways of writing the code in between, but what arrives and what leaves is the same text, defined by HTTP. It pays to know that text exactly, because every bug in an API is in the end a response that says the wrong thing, and every feature is a request carrying something new. This lesson builds the most honest server there is: one that tells the client exactly what it received.

A request, as it travels

Here is a request to create a cat, as it looks on the wire:

POST /cats?notify=true HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json

{"name": "Luna", "age": 5}

It has four parts:

  • The method, POST, says what the client wants done. GET reads, POST creates, and there are a few more, which lesson 2 is about.
  • The target, /cats?notify=true: a path naming what the request is about, then an optional query string of key=value pairs after the ?, joined by &.
  • Headers, one per line as Name: value. Names are case-insensitive: accept and Accept are the same header. Content-Type says what the body is; Accept says what the client can read back.
  • The body, after a blank line. It is optional, and a GET has none. It is only bytes: Content-Type is the one thing saying they are JSON.

A response, as it travels

HTTP/1.1 201 Created
Content-Type: application/json
Location: /cats/3

{"id": 3, "name": "Luna", "age": 5}

A status line with a three-digit code and a phrase for humans, then headers, then a body. The status is what a client checks first; lesson 3 is about choosing it well. Headers carry everything that is not the content itself: its type, its language, how long it may be cached, where a new resource lives.

The same parts in Hono

Hono hands every handler a context, c. Its c.req reads the request, part by part:

On the wireIn Hono
the methodc.req.method
the pathc.req.path
one query parameter, or all of themc.req.query('page'), c.req.query()
a headerc.req.header('accept'), undefined when absent
the body as textawait c.req.text(), "" when there is none

The response is built by a helper on c: c.text(body) or c.json(value), with the status as a second argument. Headers are set before the helper runs, with c.header(name, value):

app.get('/time', (c) => {
  c.header('cache-control', 'no-store');
  return c.json({ now: new Date().toISOString() });
});

app.get() answers one method. app.all(path, handler) answers every method, which is what a server that only describes requests needs.

Query values are always strings, since the target is text: ?age=3 gives "3", not 3. And a header that was not sent is undefined, which matters when you put it in JSON: JSON.stringify drops a key whose value is undefined, so the key disappears instead of saying "absent". null survives, so c.req.header('accept') ?? null is how absence stays visible.

HEAD, for free

A HEAD request asks for exactly the headers a GET would get, and no body. Clients use it to check that something exists, or how large it is, without downloading it. You do not write a HEAD route: Hono answers HEAD from the GET route and drops the body, so every header you set on the GET is on the HEAD too.

Your task

  1. /echo answers every method with a JSON description of the request it received: method, path, query (every query parameter), contentType and accept (those two request headers, null when absent), and body (the body as text, "" when there is none).
  2. GET /greeting answers Hello, <name> as text for ?name=<name>, and Hello, stranger without one, with a content-language: en header and a cache-control: no-store header.

Then use the request bar: pick a method, type a path, put headers in the headers box one per line, and look at what /echo says came in. Send a body with content-type: text/plain and see that it is just text.

When it fails

  • The contentType or accept key is missing from the echo: the header was absent, the value was undefined, and JSON dropped the key. Use ?? null.
  • body is an object, or the GET echo fails with a 500: you read it with c.req.json(), which parses, and throws when there is nothing to parse. The task asks for the text, c.req.text().
  • query is "Tom" instead of an object: c.req.query('name') reads one parameter; c.req.query() reads them all.
  • The HEAD request is missing a header: it was set after return, or only on some branch. Set headers before returning, on every path through the handler.

Remember

  • A request is a method, a target (path and query), headers and an optional body; a response is a status, headers and a body.
  • Header names are case-insensitive, query values are strings, and a body is bytes whose meaning Content-Type declares.
  • undefined vanishes from JSON; ?? null keeps an absent value visible.
  • HEAD gets a GET's headers without the body, and Hono answers it from the GET route.
Stuck? Show a hint

app.all('/echo', …) answers every method. c.req.query() with no argument gives every query parameter as an object; c.req.header('accept') is undefined when the header is absent, and ?? null turns that into null, which JSON keeps. await c.req.text() reads the body as text, and gives "" when there is none. Headers set with c.header() before returning c.text() end up on the response.