Documentation
Standard Schema
Kynetra FX supports the Standard Schema v1 specification. Any library that implements the ~standard protocol — including Zod, Valibot, and ArkType — can be used as input, query, or output schemas in a contract without Kynetra FX importing those libraries.
What is Standard Schema?#
Standard Schema is a community specification that lets framework authors accept any conforming validator without coupling to a specific library. Instead of depending on Zod, Valibot, or ArkType directly, a framework can call a single protocol method and get back a normalized result.
The protocol is defined by a ~standard symbol property on the validator object. When a schema is passed to app.route(), Kynetra FX checks for this symbol. If the property is present, the schema is treated as Standard Schema v1–compliant and FX delegates all parsing and validation to it.
This means you bring your own schema library and version. FX does not pin or bundle any validator; it only speaks the protocol.
Using Zod#
Install Zod in your project, define a schema, and pass it directly to input in app.route(). The handler receives ctx.input typed as z.infer<typeof schema> — no extra casting needed.
npm install zodimport { app } from '@kynetra/fx'import { z } from 'zod' const createUserSchema = z.object({ name: z.string().min(1), email: z.string().email(), role: z.enum(['admin', 'member', 'viewer']).default('member'),}) app.route({ method: 'POST', path: '/users', input: createUserSchema, handler(ctx) { // ctx.input is typed as z.infer<typeof createUserSchema> const { name, email, role } = ctx.input return ctx.json({ id: crypto.randomUUID(), name, email, role }, 201) },})If the request body fails Zod validation, FX returns HTTP 422 with the normalized FX_VALIDATION_ERROR shape. See Validation for the full error format.
Using Valibot#
Valibot schemas also implement Standard Schema v1. Import the library, compose a schema with v.object, and pass it to input. Use Valibot's InferOutput<typeof schema> utility when you need to reference the inferred type outside the handler.
npm install valibotimport { app } from '@kynetra/fx'import * as v from 'valibot'import type { InferOutput } from 'valibot' const createPostSchema = v.object({ title: v.pipe(v.string(), v.minLength(1), v.maxLength(120)), body: v.string(), tags: v.array(v.string()), published: v.optional(v.boolean(), false),}) type CreatePost = InferOutput<typeof createPostSchema> app.route({ method: 'POST', path: '/posts', input: createPostSchema, handler(ctx) { const post: CreatePost = ctx.input return ctx.json({ id: crypto.randomUUID(), ...post }, 201) },})Using ArkType#
ArkType's type() function returns an object that conforms to Standard Schema v1. Define your schema with ArkType's string-based syntax and pass it straight to input.
npm install arktypeimport { app } from '@kynetra/fx'import { type } from 'arktype' const productSchema = type({ name: 'string', price: 'number', sku: 'string', inStock: 'boolean',}) app.route({ method: 'POST', path: '/products', input: productSchema, handler(ctx) { // ctx.input inferred from ArkType's inference const { name, price, sku, inStock } = ctx.input return ctx.json({ id: crypto.randomUUID(), name, price, sku, inStock }, 201) },})Mixing fx and third-party schemas#
You are not required to commit to a single schema library across your entire application. FX's built-in schema builder (see Schemas) and any Standard Schema–compliant library coexist without conflict.
A common pattern is to use the FX builder for simple contracts and Zod or Valibot for routes that need richer validation logic such as refinements, transforms, or cross-field checks.
import { app, s } from '@kynetra/fx'import { z } from 'zod' // Built-in fx schema for a simple read routeapp.route({ method: 'GET', path: '/health', output: s.object({ ok: s.boolean() }), handler(ctx) { return ctx.json({ ok: true }) },}) // Zod schema for a route that needs transformsconst registerSchema = z.object({ email: z.string().email().toLowerCase(), password: z.string().min(12), confirmPassword: z.string(),}).refine((d) => d.password === d.confirmPassword, { message: 'Passwords do not match', path: ['confirmPassword'],}) app.route({ method: 'POST', path: '/auth/register', input: registerSchema, handler(ctx) { const { email, password } = ctx.input return ctx.json({ email, created: true }, 201) },})Output schemas#
The output field in app.route() accepts the same Standard Schema v1 validators as input and query. However, the output schema is not validated at runtime. It serves two purposes:
- OpenAPI document generation — FX uses the output schema to produce the response schema in the generated spec.
- Typed client response types — the typed client derives the success response type from the output schema so callers get accurate autocompletion.
Because no runtime validation occurs on responses, you can use any Standard Schema library for output without a performance concern. See Contracts for the full route definition API.
import { app } from '@kynetra/fx'import { z } from 'zod' const userOutputSchema = z.object({ id: z.string().uuid(), name: z.string(), email: z.string().email(), createdAt: z.string().datetime(),}) app.route({ method: 'GET', path: '/users/:id', output: userOutputSchema, // used for OpenAPI + typed client only async handler(ctx) { const user = await db.users.find(ctx.params.id) return ctx.json(user) },})How it works#
When Kynetra FX receives a request on a route with an input or query schema, it calls:
schema[Symbol.for('~standard')].validate(data)The return value is either a success object containing the parsed value, or a failure object containing an array of issues. FX normalizes failure issues into the FX_VALIDATION_ERROR shape with a stable location, path, and message per issue, then responds with HTTP 422.
The validated, coerced value returned by the schema (not the raw input) is what FX assigns to ctx.input or ctx.validatedQuery. This means Zod transforms and Valibot pipes run before your handler sees the data. See Validation for the full error shape and behavior details.
Warning
Symbol.for('~standard') protocol — the library itself must be present at runtime.