h3 and Nitro route handlers
@drzl/generator-h3 emits h3 event handlers from your Drizzle schema: one module per table, every payload validated through h3's own validation helpers, for Nitro and Nuxt.
The version split, which is the whole design
Established from the registry and from reading both type surfaces on 2026-08-11:
| Version | What it is | |
|---|---|---|
h3@1x | 1.15.11 | what nitropack 2.13.4 depends on: h3: ^1.15.11 |
h3@latest | 2.0.1-rc.26 | a release candidate |
So npm install h3 gets a version no released Nitro uses, and every real Nuxt application is on v1. h3: 'v1' is the default here for that reason.
That is the opposite call from the MCP generator, which defaults to its newer SDK, and the difference is worth stating: there the older SDK could not carry two of the three validation libraries at all, and here the older major is simply what people have installed.
The two differ in a way that reaches the emitted code:
- v2 takes a Standard Schema directly:
readValidatedBody(event, InsertusersSchema). It also addsdefineValidatedHandler, which declares body, query and headers up front. - v1 takes a
ValidateFunction<T>, which is(data: unknown) => T | true | false | void, with no Standard Schema overload at all. h3's own documentation suggests passingobjectSchema.safeParse, which is a zod-shaped idiom valibot and arktype do not have.
So a v1 module carries a small adapter and a v2 module carries none. Both are compiled against the real h3 of their major in this package's tests, and a case asserts that handing a schema straight to v1 still fails, so the adapter cannot quietly become dead weight.
One line of that adapter is load-bearing
if (result.issues) {
throw createError({ statusCode: 400, statusMessage: 'Bad Request', data: { issues: result.issues } });
}
return result.value;The failure test is result.issues and not 'value' in result. Valibot's failure result carries a value key alongside its issues, so the second form reports every valibot failure as a success. That is not hypothetical: the Vercel AI SDK does exactly that, which is why @drzl/generator-ai emits a workaround for it. issues is the discriminator the Standard Schema specification actually defines.
Setup
npm install -D @drzl/generator-h3// drzl.config.ts
export default {
schema: './src/db/schema.ts',
generators: [
{ kind: 'zod', path: 'src/validators/zod' },
{ kind: 'h3', path: 'server/generated' },
],
};Set h3: 'v2' when your project is on the 2.x line. This generator emits no schemas of its own, so validation.useShared is not a choice and the CLI turns it on; the import path is derived from the sibling generator's own path.
What is emitted
Per table:
| Handler | Route |
|---|---|
listUsers | GET /users |
getUsers | GET /users/:id |
createUsers | POST /users |
updateUsers | PATCH /users/:id |
deleteUsers | DELETE /users/:id |
A table with no primary key keeps list and create. A materialized view keeps list and get, because the database refuses every write to it.
A numeric path segment is converted, not declared: a segment is always a string, so z.number() against "1" refuses every request. Not z.coerce.number() either, which accepts an empty string as 0. The same grid the Hono generator measured.
The params schema and the query bounds are this generator's own inventions: no validation generator emits either, because only a router knows that these columns arrive as path segments.
The barrel is a route table, not a mounted app
Deliberately. Nitro discovers handlers by file path under server/routes, Nuxt under server/api, and a bare h3 project mounts them on a router by hand. Any one of those three would be wrong for the other two, so the barrel re-exports every handler and lists where each belongs, and your project decides how to mount them.
For Nitro, that usually means a one-line file per route:
// server/routes/users/index.get.ts
export { listUsers as default } from '../../generated/users';Options
| Option | Default | What it does |
|---|---|---|
path | outDir | Where the modules are written |
h3 | 'v1' | Which h3 major to emit for |
validation.library | zod | Which sibling generator's schemas the handlers validate with |
validation.importPath | derived | Where those schemas live, when it is not the sibling's path |
naming.routerSuffix | none | Appended to each module name and handler name |
naming.procedureCase | none | Casing for file names, identifiers and the URL segment |

