@kaapi/validator-valibot
v0.0.42
Published
Valibot-powered request validation and documentation plugin for Kaapi.
Maintainers
Readme
🧪 @kaapi/validator-valibot
Valibot-powered validation plugin for Kaapi. Validate request params, payload, query, headers, and state using Valibot schemas. Includes built-in documentation helpers for seamless API docs generation.
🚀 Installation
npm install @kaapi/validator-valibot📦 Peer Dependency
Requires Valibot:
npm install valibot@^1.1.0🛠️ Usage
🔌 Register the Plugin
import { Kaapi } from '@kaapi/kaapi'
import { validatorValibot } from '@kaapi/validator-valibot'
const app = new Kaapi({
port: 3000,
host: 'localhost',
docs: {
disabled: false // explicitly enables documentation generation
}
});
await app.extend(validatorValibot); // register the plugin📐 Define a Schema
import * as v from 'valibot'
import { ValidatorValibotSchema } from '@kaapi/validator-valibot'
const routeSchema: ValidatorValibotSchema = {
payload: v.object({
name: v.string()
})
}🧭 Create a Route
app.base().valibot(routeSchema).route(
{
method: 'POST',
path: '/items'
},
req => ({ id: Date.now(), name: req.payload.name })
)
// or using inline handler
/*
app.base().valibot(routeSchema).route({
method: 'POST',
path: '/items',
handler: req => ({ id: Date.now(), name: req.payload.name })
})
*/🧱 Build Routes Separately with withSchema
You can use withSchema to create validated routes without directly chaining from app.base().
This cleanly separates route construction from app registration.
import { withSchema } from '@kaapi/validator-valibot'
import * as v from 'valibot'
const schema = {
payload: v.object({
name: v.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-valibot when building modular APIs.
⚙️ Advanced Configuration
🔧 options
Customize Valibot parsing behavior:
| Property | Type | Default | Description |
|---------------|---------------------------|-------------|-----------------------------------------------------------------------------|
| abortEarly | boolean | undefined | Whether it should be aborted early |
| abortPipeEarly | boolean | undefined | Whether a pipe should be aborted early |
| lang | string | undefined | The selected language |
| message | ErrorMessage<TIssue> | undefined | The error message |
🚨 failAction
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 |
🧪 Example with Overrides
You can override Valibot validation behavior globally for all routes, or per route as needed.
🔁 Global Override (All Routes)
const app = new Kaapi({
// ...
routes: {
plugins: {
valibot: {
options: {
abortEarly: true
},
failAction: 'log'
}
}
}
});
await app.extend(validatorValibot);This sets abortEarly to true for all Valibot-validated routes, and logs validation errors before throwing them.
🔂 Per-Route Override
app.base().valibot({
query: v.object({
name: v.optional(v.pipe(v.string(), v.trim(), v.nonEmpty(), v.maxLength(10), v.description('Optional name to personalize the greeting response')), 'World'),
age: v.optional(
v.pipe(
v.string(),
v.transform((input) => typeof input === 'string' ? Number(input) : input),
v.number(),
v.integer(),
v.minValue(1)
)
)
}),
options: {
abortEarly: 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 Valibot validation is supported. Here's how to validate an uploaded image file and stream it back in the response:
app.base().valibot({
payload: v.object({
file: v.pipe(
v.looseObject({
_data: v.instance(Buffer),
hapi: v.looseObject({
filename: v.string(),
headers: v.looseObject({
'content-type': v.picklist(['image/jpeg', 'image/jpg', 'image/png'] as const)
})
})
}),
v.description('The image to upload')
)
})
}).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'])
);🧾 Notes
looseObjectis used to accommodate the structure of multipart file metadata.- The
_data: 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 Valibot via app.base().valibot(...).route(...). This dual-mode support ensures graceful evolution, allowing traditional and modern routes to coexist without breaking changes.
📚 License
MIT
This package is tested as part of the Kaapi monorepo. See the main Kaapi README for coverage details.
🤝 Contributing
Contributions, issues, and feature requests are welcome! Feel free to open a discussion or submit a pull request.
