Files
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
Requests and responses
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.GETreads,POSTcreates, 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 ofkey=valuepairs after the?, joined by&. - Headers, one per line as
Name: value. Names are case-insensitive:acceptandAcceptare the same header.Content-Typesays what the body is;Acceptsays what the client can read back. - The body, after a blank line. It is optional, and a
GEThas none. It is only bytes:Content-Typeis 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 wire | In Hono |
|---|---|
| the method | c.req.method |
| the path | c.req.path |
| one query parameter, or all of them | c.req.query('page'), c.req.query() |
| a header | c.req.header('accept'), undefined when absent |
| the body as text | await 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
/echoanswers every method with a JSON description of the request it received:method,path,query(every query parameter),contentTypeandaccept(those two request headers,nullwhen absent), andbody(the body as text,""when there is none).GET /greetinganswersHello, <name>as text for?name=<name>, andHello, strangerwithout one, with acontent-language: enheader and acache-control: no-storeheader.
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
contentTypeoracceptkey is missing from the echo: the header was absent, the value wasundefined, and JSON dropped the key. Use?? null. bodyis an object, or the GET echo fails with a 500: you read it withc.req.json(), which parses, and throws when there is nothing to parse. The task asks for the text,c.req.text().queryis"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-Typedeclares. undefinedvanishes from JSON;?? nullkeeps 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.
Press Run tests to start the app. Its log appears here.Graded endpoints
The route the starter already has: status 200 and a text body
Method, path and every query parameter, as strings; a GET carries no body and no content-type, so body is "" and contentType is null
The body arrives as text; content-type is the only thing saying it is JSON
A body does not have to be JSON: this one is plain text, and says so
No query, no headers, no body: the absent headers are null, not missing keys
A query parameter read into a text body, with the two response headers the task asks for
Without the parameter the greeting has a default, and the same headers
The same headers a GET would get and no body, answered by the GET route with nothing extra written