Documentation
Validation
Kynetra FX validates request data at the boundary — before the handler runs — when a contract defines input or query schemas. Validation failures produce a stable, machine-readable 422 response.
When validation runs#
FX validates selectively based on the method and which schema fields are present in the contract definition:
| Name | Type | Description |
|---|---|---|
| input | POST · PUT · PATCH | Validates the request body when an input schema is set. Body must be valid JSON. Other methods ignore input even if a schema is defined. |
| query | all methods | Validates parsed query string parameters when a query schema is set. Applies to GET, POST, DELETE, and all other methods. |
| output | never (runtime) | Not validated at runtime. Used only for OpenAPI generation and typed client response types. |
If neither input nor query is set on a route, no validation runs and the handler is called immediately with the raw context.
Note
output schema is used only for OpenAPI generation and typed client response types.The 422 error shape#
Every validation failure returns HTTP 422 with a JSON body that follows a stable, typed structure. This shape is the same regardless of which schema library produced the error.
{ "error": { "code": "FX_VALIDATION_ERROR", "message": "Request validation failed", "details": { "issues": [ { "location": "body", "path": ["address", "zip"], "message": "Expected string, received undefined" } ] } }}Each object in the issues array describes one validation failure. FX collects all issues before responding — it does not stop at the first failure.
| Name | Type | Description |
|---|---|---|
| location | 'body' | 'query' | Where the issue originated. body for input schema failures, query for query schema failures. |
| path | (string | number)[] | JSON path to the failing field. An empty array means the root value failed. Array indices appear as numbers (e.g. ["items", 0, "name"]). |
| message | string | Human-readable description of the validation failure, forwarded from the underlying schema library. |
Body validation#
Body validation runs on POST, PUT, and PATCH requests when an input schema is set. The raw request body is parsed as JSON before being handed to the schema. If the body is not valid JSON — empty, malformed, or the wrong content type — FX returns HTTP 422 (not 400) before the schema is even invoked.
# Missing required fieldcurl -X POST https://api.example.com/users \ -H 'Content-Type: application/json' \ -d '{"name": "Ada"}'{ "error": { "code": "FX_VALIDATION_ERROR", "message": "Request validation failed", "details": { "issues": [ { "location": "body", "path": ["email"], "message": "Required" } ] } }}# Malformed JSON bodycurl -X POST https://api.example.com/users \ -H 'Content-Type: application/json' \ -d 'not json at all'{ "error": { "code": "FX_VALIDATION_ERROR", "message": "Request validation failed", "details": { "issues": [ { "location": "body", "path": [], "message": "Invalid JSON" } ] } }}Query validation#
Query validation runs on any HTTP method when a query schema is set. Query string values are always strings at the HTTP layer; your schema is responsible for coercing types (e.g. parsing "42" into a number). Both the FX built-in schema builder and Standard Schema–compliant libraries work for query schemas.
Validated query parameters are available as ctx.validatedQuery inside the handler, typed according to the schema's inferred output type.
import { app } from '@kynetra/fx'import { z } from 'zod' const listQuerySchema = z.object({ page: z.coerce.number().int().min(1).default(1), limit: z.coerce.number().int().min(1).max(100).default(20), category: z.string().optional(),}) app.route({ method: 'GET', path: '/products', query: listQuerySchema, async handler(ctx) { // ctx.validatedQuery is typed as z.infer<typeof listQuerySchema> const { page, limit, category } = ctx.validatedQuery const products = await db.products.list({ page, limit, category }) return ctx.json({ products, page, limit }) },})# page must be a positive integercurl 'https://api.example.com/products?page=0&limit=20'{ "error": { "code": "FX_VALIDATION_ERROR", "message": "Request validation failed", "details": { "issues": [ { "location": "query", "path": ["page"], "message": "Number must be greater than or equal to 1" } ] } }}Multiple issues#
FX does not stop at the first validation failure. All issues from the schema are collected and returned together in a single 422 response. This lets clients surface all problems to the user at once rather than correcting errors one at a time.
{ "error": { "code": "FX_VALIDATION_ERROR", "message": "Request validation failed", "details": { "issues": [ { "location": "body", "path": ["email"], "message": "Invalid email address" }, { "location": "body", "path": ["password"], "message": "String must contain at least 12 characters" }, { "location": "body", "path": ["role"], "message": "Invalid enum value. Expected 'admin' | 'member' | 'viewer'" } ] } }}The number of issues returned depends on what the underlying schema library reports. FX forwards all issues without truncation.
Handling validation errors on the client#
The typed client distinguishes success and error responses through the ok discriminant. When a request fails with 422, ok is false and the response data contains the full error shape. TypeScript narrows the type automatically once you check ok.
import { createClient } from '@kynetra/fx/client'import type { App } from '../src/app' const client = createClient<App>({ baseUrl: 'https://api.example.com' }) async function createUser(data: unknown) { const res = await client.POST['/users'](data) if (!res.ok) { if (res.status === 422) { // res.data.error is typed as FXValidationError const issues = res.data.error.details.issues for (const issue of issues) { console.error( `[${issue.location}] ${issue.path.join('.')} — ${issue.message}` ) } } return null } // res.data is typed as the output schema inferred type return res.data}Custom error handling#
If you need to transform or replace the default 422 response — for example to match a different error envelope, add a request ID, or log to an external service — you can intercept validation errors with an onError hook. The hook receives an FXError whose code is FX_VALIDATION_ERROR for validation failures.
import { app } from '@kynetra/fx' app.onError((err, ctx) => { if (err.code === 'FX_VALIDATION_ERROR') { return ctx.json( { type: 'validation_error', requestId: ctx.requestId, fields: err.details.issues.map((i) => ({ field: i.path.join('.'), message: i.message, })), }, 422 ) } // let other errors fall through to the default handler return ctx.json({ error: err.message }, 500)})See Hooks for the full onError API and other lifecycle hooks available in Kynetra FX.