Logging and request idsMiddleware · Hono

Give every request a log line, an id that follows it from the client through the handler to the response, and a JSON body a human can read when asked.

What you will learn

Read the theory for Logging and request ids

All Middleware lessons

All Hono courses

loading types…

What you'll learn

  • Log every request and response with logger(), and route the lines somewhere with its print function
  • Tag every request with requestId(): reuse the id a client sent, generate one otherwise, expose it on the response and on the context
  • Prettify JSON on demand with prettyJSON() and its ?pretty query

Logging and request ids

The Basics course ended with an API that works. The first thing an API that runs for other people needs is not another route but a way to see what happened: which requests came in, what they got back, how long it took, and, when a client reports a problem, which of the thousand requests of that minute was theirs. Hono ships three small middleware for that, and this course opens with them because every later lesson's console will read better with them in place.

logger()

import { logger } from 'hono/logger';

app.use(logger());
app.get('/', (c) => c.text('Hello Hono!'));

Two lines per request: <-- GET / when it arrives and --> GET / 200 3ms when the response goes out, with the path including its query string and the time in milliseconds or, past a second, in seconds. On a terminal the status is coloured; the NO_COLOR environment variable turns that off, and platforms without process.env print plain text.

By default the lines go to console.log. logger(fn) takes a print function instead, (message, ...rest) => void, which is how the lines reach a log service, a file, or a ring buffer a debug route can show. The docs point out the same function is useful for your own lines, customLogger('Blog saved:', 'ID: 1'), so they land in the same place with the same shape.

requestId()

A log line alone cannot be matched to a client's complaint. An id that travels with the request can: the client sends it, every log line carries it, the response returns it.

import { requestId } from 'hono/request-id';

app.use('*', requestId());
app.get('/', (c) => c.text(`Your request id is ${c.get('requestId')}`));

The middleware reads X-Request-Id from the request. When the client sent one, that value is used; when it did not, crypto.randomUUID() makes one. Either way the id goes on the response as X-Request-Id and on the context as requestId, where handlers and later middleware read it with c.get(). An incoming value is only reused when it is short enough (limitLength, 255 by default) and made of letters, digits, _, - and =; anything else is replaced, so a client cannot inject arbitrary text into your logs through the header. headerName renames the header, and an empty string stops the middleware reading or writing it while still setting the variable; generator replaces the UUID, which is how a platform's own id, Cloudflare's ray id or AWS Lambda's request id, becomes the app's.

The type of c.get('requestId') comes from RequestIdVariables, exported by the module: new Hono<{ Variables: RequestIdVariables }>(), the same Variables generic as in Basics.

prettyJSON()

Compact JSON is right for programs and hard on people. prettyJSON() re-serialises any JSON response with two-space indentation when the request carries ?pretty, and leaves every other response alone:

import { prettyJSON } from 'hono/pretty-json';

app.use(prettyJSON());
app.get('/', (c) => c.json({ message: 'Hono!' }));

GET /?pretty answers the indented form. space changes the indentation, query the parameter name, and force: true prettifies every JSON response, which is convenient in development and a waste of bytes in production.

Order

All three are registered above the routes, and their order among themselves is the order of the onion from Basics: logger() first sees the request first and the response last, which is what a timing needs. requestId() before anything that logs means the id exists when the first line is written.

Your task

The cats API has a /log route that shows an empty array.

  1. Log every request and response with logger(), keeping each line in the log array as well as printing it, so /log shows them.
  2. Tag every request with requestId(), reusing the client's X-Request-Id when there is one.
  3. Answer GET /cats/:id with the cat plus the request's id under requestId.
  4. Prettify JSON responses with prettyJSON() when the request asks with ?pretty.

Send GET /cats?pretty from the request bar, then GET /log.

When it fails

  • /log stays empty while the console shows the lines: logger() was given no function, so the lines went only to console.log. Pass a function that does both.
  • The response carries a different x-request-id than the one sent: the id sent contained a space or a character outside the allowed set, or was over limitLength, and was replaced.
  • c.get('requestId') is a type error: the app was created without RequestIdVariables in its generic.
  • GET /cats?pretty is still compact: prettyJSON() was registered after the route, or the response is not application/json.

Remember

  • logger() prints an incoming and an outgoing line per request; logger(fn) sends them elsewhere.
  • requestId() reuses a valid incoming X-Request-Id or generates one, on the response and on c.get('requestId'); type the app with RequestIdVariables.
  • prettyJSON() indents JSON when the request has ?pretty.
  • Register all three above the routes; the logger first.
Stuck? Show a hint

logger(fn) calls fn(message, ...rest) for each line; a function that pushes the joined line into an array and also prints it does both jobs. requestId() reads X-Request-Id, sets the same header on the response and c.get('requestId'); type the app with RequestIdVariables. prettyJSON() acts on any JSON response when the request has ?pretty.