Hono foundations

Use context deliberately

Read request data, set response metadata, and pass typed per-request values through Hono context.

The handler receives a context object, usually named c. It wraps one request. Read from it, write to it, return a response from it.

Authentication middleware can store a user id for later handlers without touching a module global:

app.use('*', async (c, next) => {
  const userId = c.req.header('x-user-id')
  if (!userId) {
    return c.json({ error: 'Unauthorized' }, 401)
  }
  c.set('userId', userId)
  await next()
})

app.get('/bookmarks', (c) => {
  const userId = c.get('userId')
  return c.json({ userId, bookmarks: [] })
})

Two curl calls with different headers should return different user ids:

curl -s http://localhost:3000/bookmarks -H 'x-user-id: user_a'
curl -s http://localhost:3000/bookmarks -H 'x-user-id: user_b'

You should see {"userId":"user_a","bookmarks":[]} and {"userId":"user_b","bookmarks":[]}. Omit the header and you get 401 {"error":"Unauthorized"}.

Context belongs to one request. Each app.fetch or app.request call gets its own instance. That beats a module global when two clients hit the API at the same time.

Type the variables when you create the app:

const app = new Hono<{ Variables: { userId: string } }>()

Now c.get('userId') is a string in TypeScript, and a typo in the key fails at compile time.

A realistic failure: you store currentUserId in module scope and assign it inside middleware. Concurrent requests overwrite each other. One client sees another user’s bookmarks intermittently. Fix by moving the value to c.set('userId', ...) only.

Vitest can prove isolation with Promise.all and two app.request() calls carrying different headers. Both should pass in the same test run.

Missing auth should fail before the handler runs:

curl -s -w '\n%{http_code}\n' http://localhost:3000/bookmarks

Expect 401, not {"userId":null,"bookmarks":[]}. If you see null in the body, middleware is optional when it should be required.

Store bindings on context the same way you store user ids: c.set('bookmarks', deps.bookmarks) in middleware keeps handlers free of globals.

Context also carries the response under construction. Timing middleware reads c.res.status only after await next() completes.

Bindings like c.set('bookmarks', store) follow the same rule as user ids: one request, one context, no module globals.

Try this on your own project: move one piece of per-request state from a global into c.set() and add the concurrent request test.

Lesson completed