Routing and middleware

Understand middleware order

Trace before-next and after-next behavior through nested middleware and handlers.

Middleware in Hono composes like an onion. Code before await next() runs on the way in. Code after it runs on the way out.

A timer middleware starts the clock before auth and records the final status after the route returns:

const timer = async (c, next) => {
  const start = Date.now()
  console.log('timer: enter')
  await next()
  const ms = Date.now() - start
  console.log(`timer: exit ${c.res.status} in ${ms}ms`)
}

app.use('*', timer)
app.use('*', authMiddleware)
app.get('/bookmarks', (c) => {
  console.log('handler')
  return c.json([])
})

For a successful GET, the log order is:

timer: enter
auth: enter
handler
auth: exit
timer: exit 200 in 3ms

Each layer wraps the next. The handler sits at the center. Outer middleware sees the final response status because the handler already ran when after-code executes.

Two mistakes show up constantly. Calling next() without await lets after-code run before the handler finishes. Skipping await next() entirely means inner middleware and the route never run.

If auth rejects the request, it returns early and the handler never logs. After-code in outer layers still runs for whatever response auth returned. That is how a timer can log 401 even when the route body never executed.

Route-level middleware follows the same rules. bookmarks.use('*', requireUser) runs only for paths under that sub-app, but the onion shape inside that scope stays identical.

Add three named middleware layers. Predict every log line for success and for a 401 from auth. Run the request and compare.

When you debug a missing header or a wrong status, draw the onion on paper. Mark where each middleware reads and writes c.res. That beats staring at registration order in a long file.

Try this on your own project: add a timer middleware and verify the exit log shows the real status code, not 200 by default.

Lesson completed