Interview preparation guide

Hono Interview Questions: Trace Middleware and Validate Requests

This Hono interview exercise asks you to trace a request through middleware, explain a rejected JSON body and distinguish application tests from deployment tests. The API and questions are original practice material. The example runs in process through app.request; it does not start a server or require a browser.

What order does middleware run in?

Hono’s middleware guide describes registration order on the way into a handler and the reverse path after awaited next calls complete. Predict the X-Flow header before reading the assertions. The outer middleware owns a request-local trace; the inner middleware adds entries only for the /labels path.

The handler is one point in that sequence. A validation response can end the downstream work before the handler runs, while surrounding middleware still finishes its awaited work. The trace makes that difference observable.

Run the request exercise

Use a new scratch directory with Node.js and install hono@4.13.13 using your preferred package manager. Save this code as exercise.mjs, then run node exercise.mjs. It was checked with Node 22.22.3 and Hono 4.13.13. The expected output is “Hono exercise passed: validation, middleware order, 404 and 500”. Later major versions should be checked against their migration guidance.

import assert from 'node:assert/strict'
import { Hono } from 'hono'
import { validator } from 'hono/validator'
import { HTTPException } from 'hono/http-exception'

const app = new Hono()
app.use('*', async (c, next) => {
  const trace = ['outer-before']
  c.set('trace', trace)
  await next()
  trace.push('outer-after')
  c.header('X-Flow', trace.join(','))
})
app.use('/labels', async (c, next) => {
  const trace = c.get('trace')
  trace.push('inner-before')
  await next()
  trace.push('inner-after')
})
app.post('/labels', validator('json', (value, c) => {
  if (!value || typeof value.name !== 'string' || !value.name.trim()) {
    return c.json({ error: 'name is required' }, 400)
  }
  return { name: value.name.trim() }
}), (c) => {
  c.get('trace').push('handler')
  return c.json(c.req.valid('json'), 201)
})
app.get('/boom', () => { throw new Error('private diagnostic') })
app.onError((error, c) => {
  if (error instanceof HTTPException) return error.getResponse()
  return c.json({ error: 'unexpected failure' }, 500)
})
app.notFound((c) => c.json({ error: 'not found' }, 404))

const send = (body, contentType = 'application/json') =>
  app.request('/labels', {
    method: 'POST',
    headers: { 'Content-Type': contentType },
    body,
  })
const good = await send(JSON.stringify({ name: '  parcel  ', ignored: true }))
assert.equal(good.status, 201)
assert.deepEqual(await good.json(), { name: 'parcel' })
assert.equal(good.headers.get('X-Flow'),
  'outer-before,inner-before,handler,inner-after,outer-after')
const bad = await send(JSON.stringify({ name: ' ' }))
assert.equal(bad.status, 400)
assert.equal(bad.headers.get('X-Flow'),
  'outer-before,inner-before,inner-after,outer-after')
const wrongType = await send(JSON.stringify({ name: 'parcel' }), 'text/plain')
assert.equal(wrongType.status, 400)
const malformed = await send('{')
assert.equal(malformed.status, 400)
const missing = await app.request('/missing')
assert.equal(missing.status, 404)
const boom = await app.request('/boom')
assert.equal(boom.status, 500)
assert.deepEqual(await boom.json(), { error: 'unexpected failure' })
console.log('Hono exercise passed: validation, middleware order, 404 and 500')

Why can a JSON-looking body fail validation?

The request body is text on the wire. Hono’s JSON validator expects a matching Content-Type. In the wrongType test, the string contains JSON syntax but the header says text/plain; the validator does not receive the intended parsed object and this application returns 400. Correct the request contract rather than weakening validation.

The successful response contains only the trimmed name returned by the validator. The input’s ignored property does not appear in the response because the handler uses c.req.valid("json"), not the original object.

Interview follow-up

Question: “Does this prove every unknown field is rejected?”

Answer: “No. This validator discards extra fields when it returns a new object. Rejecting unknown fields would be a different contract that needs its own check.”

Question: “Does a successful TypeScript compilation validate a request body?”

Answer: “No. Runtime inputs still need checks. This example is JavaScript and makes that boundary explicit.”

How do errors differ from missing routes?

The /missing assertion checks the application’s notFound response. Malformed JSON raises a known Hono HTTPException; onError preserves its response through getResponse(), and the malformed assertion verifies status 400. The /boom route throws an unexpected error, which still receives a generic 500 body. A client parsing error should not be turned into a server failure by a catch-all handler.

The private diagnostic is deliberately absent from the response. A real service would also need an appropriate diagnostic logging strategy; the exercise does not claim to implement one. Likewise, name validation is not authentication or authorization.

Boundary questions to practice

What should happen for a missing name, a numeric name or a whitespace-only string? Under this contract, each should be rejected.

What is the difference between returning a response and awaiting next? Returning a response ends that downstream path; awaiting next allows later matching middleware or a handler to run.

Why use a trace stored on the context? It belongs to this request. A mutable global trace could mix concurrent requests and invalidate the demonstration.

What does app.request prove, and what is still untested?

Hono documents app.request as a way to pass requests into the application and inspect responses. Here it verifies status codes, selected response bodies and the middleware trace in one process. It does not verify a Node server adapter, a deployed edge environment, a reverse proxy, network timeouts or browser CORS behavior.

If an interviewer changes the scenario to a deployed API, identify the runtime and boundaries that need an integration test. Keep the small application tests because they make contract regressions easy to diagnose; add deployment checks for behavior that only exists outside the process.

Questions, answered

Do I need a browser to run this example?

No. It uses app.request in Node and inspects Response objects directly.

Why is Content-Type included in the tests?

It is part of the request contract. A JSON-looking body with a different content type does not exercise the same parsing path.

Is this a production-ready API?

No. It is a focused interview exercise. Authentication, authorization, persistence, request-size limits and deployment behavior are outside its scope.

Sources and examples

ResumizeAI team. Examples are fictional teaching material, not customer results. Replace sample details with your own facts.

Your experience. A complete resume.

Build in Resumize with ATS-friendly templates, guided editing and a live preview. Start free, then upgrade when you need more.

Free account required. Free-plan PDF exports include a watermark.