Validation and errors
Validate the correct target
Validate JSON, form, query, header, cookie, or parameter input where it actually enters the route.
Hono validation is target-specific. JSON, query, param, header, and cookie each have their own validator hook. Pick the target that matches your API contract.
Bookmark creation validates the JSON body. Search validates query values. The id param becomes a bounded identifier:
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const createBookmark = z.object({
title: z.string().min(1).max(120),
url: z.string().url()
})
const searchQuery = z.object({
q: z.string().min(1).max(80)
})
const bookmarkId = z.object({
id: z.string().regex(/^[a-z0-9-]{8,32}$/i)
})
app.post('/bookmarks', zValidator('json', createBookmark), (c) => {
const body = c.req.valid('json')
return c.json({ id: 'bk_01', ...body }, 201)
})
app.get('/bookmarks/search', zValidator('query', searchQuery), (c) => {
const { q } = c.req.valid('query')
return c.json({ q, results: [] })
})
app.get('/bookmarks/:id', zValidator('param', bookmarkId), (c) => {
const { id } = c.req.valid('param')
return c.json({ id, title: 'Hono docs', url: 'https://hono.dev' })
})
Search via query string:
curl -s 'http://localhost:3000/bookmarks/search?q=hono'
You should get {"q":"hono","results":[]}. Empty q returns 400 with validator JSON.
Create via JSON body:
curl -s -w '\n%{http_code}\n' -X POST http://localhost:3000/bookmarks \
-H 'content-type: application/json' \
-d '{"title":"Hono docs","url":"https://hono.dev"}'
Expect 201 and a bookmark object. A realistic failure: you validate JSON but read c.req.query('title') in the handler because a client sent both. A spoofed query param bypasses the body contract. Fix by using only c.req.valid('json') after validation.
Content type matters. Sending form data to a JSON validator returns 400 even when field names match:
curl -s -w '\n%{http_code}\n' -X POST http://localhost:3000/bookmarks \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'title=Hono+docs&url=https%3A%2F%2Fhono.dev'
That should fail until you add zValidator('form', ...).
List every input surface in your route table: param, query, json, header, cookie. Vitest should cover each target separately.
Param validation rejects malformed ids before your handler runs:
curl -s -w '\n%{http_code}\n' http://localhost:3000/bookmarks/not-valid!!!
Expect 400, not 200 with a confusing handler error. The validator message should mention the id field.
In tests, keep one case per target so a regression in query validation cannot hide behind passing JSON tests. Name tests after the surface: rejects empty search query, rejects invalid bookmark id param.
When you add a new header like x-api-key, add zValidator('header', ...) on the routes that require it. Do not read c.req.header('x-api-key') raw in the handler after skipping validation.
Search and create routes validate different targets on purpose. Mixing them in one handler is how spoofed query params slip past JSON contracts.
Try this on your own project: add one validator per input surface and grep handlers for raw c.req.json() calls that bypass it.
Lesson completed