Zod-powered request validation and documentation plugin for Kaapi.
npm install @kaapi/validator-zodparams, payload, query, headers, and state using Zod 4 schemas. Includes built-in documentation helpers for seamless API docs generation.
bash
npm install @kaapi/validator-zod
`
$3
Requires Zod v4:
`bash
npm install zod@^4.0.0
`
---
๐ ๏ธ Usage
$3
`ts
import { z } from 'zod/v4'
import { Kaapi } from '@kaapi/kaapi'
import { validatorZod } from '@kaapi/validator-zod'
const app = new Kaapi({
port: 3000,
host: 'localhost',
docs: {
disabled: false // explicitly enables documentation generation
}
});
await app.extend(validatorZod); // register the plugin
`
---
$3
`ts
import { z } from 'zod/v4'
import { ValidatorZodSchema } from '@kaapi/validator-zod'
const routeSchema: ValidatorZodSchema = {
payload: z.object({
name: z.string()
})
}
`
---
$3
`ts
app.base().zod(routeSchema).route(
{
method: 'POST',
path: '/items'
},
req => ({ id: Date.now(), name: req.payload.name })
)
// or using inline handler
/*
app.base().zod(routeSchema).route({
method: 'POST',
path: '/items',
handler: req => ({ id: Date.now(), name: req.payload.name })
})
*/
`
---
$3
You can use withSchema to create validated routes without directly chaining from app.base().
This cleanly separates route construction from app registration.
`ts
import { withSchema } from '@kaapi/validator-zod'
import { z } from 'zod/v4'
const schema = {
payload: z.object({
name: z.string()
})
}
const route = withSchema(schema).route({
method: 'POST',
path: '/items',
handler: req => ({ id: Date.now(), name: req.payload.name })
})
// later, during app setup
app.route(route)
`
This is the most flexible and convenient way to use @kaapi/validator-zod when building modular APIs.
---
โ๏ธ Advanced Configuration
$3
Customize Zod parsing behavior:
| Property | Type | Default | Description |
|---------------|---------------------------|-------------|-----------------------------------------------------------------------------|
| error | errors.$ZodErrorMap | undefined | Custom error map for localization or formatting |
| reportInput | boolean | false | When true, includes original input in error issues (useful for debugging) |
| jitless | boolean | false | When true, disables JIT optimizations for environments where eval is restricted (e.g., Cloudflare Workers). |
---
$3
Control how validation failures are handled:
| Value | Behavior | Safe? | Description |
|---------------|------------------------------|-------|--------------------------------------------------|
| 'error' | Reject with validation error | โ
| Default safe behavior |
| 'log' | Log and reject | โ
| Useful for observability |
| function | Custom handler | โ
(developer-controlled) | Must return or throw explicitly |
| 'ignore' | โ Not supported | โ | Unsafe and not implemented |
---
$3
You can override Zod validation behavior globally for all routes, or per route as needed.
#### ๐ Global Override (All Routes)
`ts
const app = new Kaapi({
// ...
routes: {
plugins: {
zod: {
options: {
reportInput: true
},
failAction: 'log'
}
}
}
});
await app.extend(validatorZod);
`
This sets reportInput to true for all Zod-validated routes, and logs validation errors before throwing them.
#### ๐ Per-Route Override
`ts
app.base().zod({
query: z.object({
name: z.string().trim().nonempty().max(10).meta({
description: 'Optional name to personalize the greeting response'
}).optional().default('World')
}),
options: {
reportInput: false
},
failAction: async (request, h, err) => {
if (Boom.isBoom(err)) {
return h.response({
...err.output.payload,
details: err.data.validationError.issues
}).code(err.output.statusCode).takeover()
}
return err
}
}).route({
path: '/greetings',
method: 'GET',
handler: ({ query: { name } }) => Hello ${name}!
});
`
---
๐ค File Upload Example
Multipart file uploads with Zod validation is supported. Here's how to validate an uploaded image file and stream it back in the response:
`ts
app.base().zod({
payload: z.object({
file: z.looseObject({
_data: z.instanceof(Buffer),
hapi: z.looseObject({
filename: z.string(),
headers: z.looseObject({
'content-type': z.enum(['image/jpeg', 'image/jpg', 'image/png'])
})
})
})
})
}).route({
method: 'POST',
path: '/upload-image',
options: {
description: 'Upload an image',
payload: {
output: 'stream',
parse: true,
allow: 'multipart/form-data',
multipart: { output: 'stream' },
maxBytes: 1024 * 3_000
}
}
}, (req, h) =>
h.response(req.payload.file._data)
.type(req.payload.file.hapi.headers['content-type'])
);
`
$3
- z.looseObject is used to accommodate the structure of multipart file metadata.
- The _data: z.instanceof(Buffer) field is automatically interpreted as a binary field by the documentation generator.
- This ensures correct OpenAPI and Postman documentation is generated, with the file field shown as a binary upload.
- The route streams the uploaded image back with its original content type.
---
๐ Flexible API Design
Prefer Joi or migrating gradually? No problem.
You can still use app.route(...) with Joi-based validation while adopting Zod via app.base().zod(...).route(...)`. This dual-mode support ensures graceful evolution, allowing traditional and modern routes to coexist without breaking changes.