Documentation
Schema Builder
The fx object exported from @kynetra/fx is a lightweight schema builder. Every schema validates at runtime, infers TypeScript types statically, and emits JSON Schema for OpenAPI.
Primitive types#
The three primitive builders cover strings, numbers, and booleans. Pair each with the Infer utility type to extract the TypeScript type without writing it twice.
import { fx, Infer } from '@kynetra/fx' const Name = fx.string() // TypeScript: stringconst Score = fx.number() // TypeScript: numberconst Active = fx.boolean() // TypeScript: boolean type Name = Infer<typeof Name> // stringtype Score = Infer<typeof Score> // numbertype Active = Infer<typeof Active> // booleanPrimitive schemas are composable — you can nest them inside fx.object(), fx.array(), or wrap them in fx.optional() without any additional configuration.
String and number constraints#
Both fx.string() and fx.number() accept .min(n) and .max(n) constraints. For strings these bound the character length; for numbers they bound the value itself.
import { fx } from '@kynetra/fx' // String length constraintsconst Username = fx.string().min(3).max(30)const Bio = fx.string().max(500) // Number value constraintsconst Age = fx.number().min(0).max(120)const Rating = fx.number().min(1).max(5)const Page = fx.number().min(1) // Constraints compose with object schemasconst CreatePostInput = fx.object({ title: fx.string().min(1).max(200), body: fx.string().min(10).max(10000), views: fx.number().min(0),})Note
.min() and .max(), both issues are reported.Enums#
fx.enum() accepts a non-empty array of string literals and narrows the TypeScript type to a union of exactly those strings. Values outside the array fail validation with a descriptive issue.
import { fx, Infer } from '@kynetra/fx' const Role = fx.enum(['admin', 'editor', 'viewer'])const Status = fx.enum(['draft', 'published', 'archived'])const Plan = fx.enum(['free', 'pro', 'enterprise']) type Role = Infer<typeof Role> // 'admin' | 'editor' | 'viewer'type Status = Infer<typeof Status> // 'draft' | 'published' | 'archived'type Plan = Infer<typeof Plan> // 'free' | 'pro' | 'enterprise' // Use inside an objectconst UpdateArticle = fx.object({ title: fx.string().min(1).max(200), status: Status,})Because the TypeScript type is a union literal, the compiler catches typos at the call site before you ever run the code.
Objects#
fx.object() takes a map of key-to-schema pairs and produces a validator that checks each key independently. The resulting Infer type is a plain TypeScript object type with all keys required by default.
import { fx, Infer } from '@kynetra/fx' const AddressSchema = fx.object({ street: fx.string().min(1).max(200), city: fx.string().min(1).max(100), zip: fx.string().min(3).max(20),}) const UserSchema = fx.object({ id: fx.string(), name: fx.string().min(1).max(100), age: fx.number().min(0).max(120), role: fx.enum(['admin', 'editor', 'viewer']), address: AddressSchema, // nested object}) type User = Infer<typeof UserSchema>// {// id: string// name: string// age: number// role: 'admin' | 'editor' | 'viewer'// address: { street: string; city: string; zip: string }// }Tip
fx.object() inside another fx.object(), or an fx.array() of objects. TypeScript inference follows the nesting automatically.Arrays#
fx.array(itemSchema) validates that the input is an array and that every element satisfies the item schema. Pass any schema as the item — primitive, enum, object, or another array.
import { fx, Infer } from '@kynetra/fx' const Tags = fx.array(fx.string()) // string[]const Scores = fx.array(fx.number().min(0)) // number[] const TagSchema = fx.object({ id: fx.string(), label: fx.string().min(1).max(50), color: fx.enum(['red', 'green', 'blue', 'gray']),}) const ArticleSchema = fx.object({ title: fx.string().min(1).max(200), tags: fx.array(TagSchema), // Tag[]}) type Article = Infer<typeof ArticleSchema>// { title: string; tags: { id: string; label: string; color: 'red' | 'green' | 'blue' | 'gray' }[] }Optional fields#
Wrap any schema with fx.optional() to make it optional inside an object. The TypeScript type adds | undefined to the field; the validator skips the inner schema when the key is absent or the value is undefined.
import { fx, Infer } from '@kynetra/fx' const UpdateUserSchema = fx.object({ // Required — must be present id: fx.string(), // Optional — may be omitted name: fx.optional(fx.string().min(1).max(100)), bio: fx.optional(fx.string().max(500)), age: fx.optional(fx.number().min(0).max(120)), role: fx.optional(fx.enum(['admin', 'editor', 'viewer'])),}) type UpdateUser = Infer<typeof UpdateUserSchema>// {// id: string// name?: string// bio?: string// age?: number// role?: 'admin' | 'editor' | 'viewer'// }Optional fields that are present but carry the wrong type still fail validation. fx.optional only permits absence, not arbitrary values.
Parsing at runtime#
Every schema has a .parse(value) method that returns a discriminated union. On success it returns { ok: true, value } where value is typed to the schema's inferred type. On failure it returns { ok: false, issues } where issues is an array describing each validation problem.
import { fx } from '@kynetra/fx' const UserSchema = fx.object({ name: fx.string().min(1).max(100), email: fx.string().min(5).max(255), age: fx.number().min(0).max(120), role: fx.enum(['admin', 'editor', 'viewer']),}) function processUser(raw: unknown) { const result = UserSchema.parse(raw) if (!result.ok) { // result.issues: array of validation errors console.error('Validation failed:', result.issues) return null } // result.value is fully typed as Infer<typeof UserSchema> const { name, email, age, role } = result.value return createUser({ name, email, age, role })}Note
input or query in a contract route, Kynetra FX calls .parse() for you and handles the ok: false branch by returning a 422 automatically. Direct calls to .parse() are useful in middleware, background workers, or anywhere outside a contract handler.JSON Schema output#
Every schema implements .jsonSchema(), which returns a plain object conforming to JSON Schema (draft 7 compatible). This is how Kynetra FX feeds the OpenAPI generator — it calls .jsonSchema() on each contract's input, query, and output schema and embeds the result in the generated spec.
import { fx } from '@kynetra/fx' const UserSchema = fx.object({ name: fx.string().min(1).max(100), age: fx.number().min(0).max(120), role: fx.enum(['admin', 'editor', 'viewer']), bio: fx.optional(fx.string().max(500)),}) console.log(JSON.stringify(UserSchema.jsonSchema(), null, 2))// {// "type": "object",// "properties": {// "name": { "type": "string", "minLength": 1, "maxLength": 100 },// "age": { "type": "number", "minimum": 0, "maximum": 120 },// "role": { "type": "string", "enum": ["admin", "editor", "viewer"] },// "bio": { "type": "string", "maxLength": 500 }// },// "required": ["name", "age", "role"]// }You can call .jsonSchema() on any schema — primitives, enums, arrays, and nested objects all produce valid JSON Schema output. See OpenAPI generation for how this integrates with the spec builder.
Using schemas in contracts#
Schemas are the building blocks of contract routes. Pass them to the input, query, and output options of app.route() to get automatic validation, TypeScript types, and OpenAPI documentation from a single definition.
import { app, fx, Infer } from '@kynetra/fx' const CreateUserBody = fx.object({ name: fx.string().min(1).max(100), email: fx.string().min(5).max(255), age: fx.optional(fx.number().min(0).max(120)), role: fx.enum(['admin', 'editor', 'viewer']),}) const UserResponse = fx.object({ id: fx.string(), name: fx.string(), email: fx.string(), role: fx.enum(['admin', 'editor', 'viewer']), createdAt: fx.string(),}) app.route({ method: 'POST', path: '/users', operationId: 'createUser', tags: ['Users'], summary: 'Create a new user account', input: CreateUserBody, output: UserResponse, handler(ctx) { const user = db.insertUser(ctx.input) return ctx.json(user, { status: 201 }) },})See Contract Routes for the full app.route() API and field reference. For details on Standard Schema compatibility — including how to use these schemas with third-party validators — see Standard Schema.
Schema method reference#
| Name | Type | Description |
|---|---|---|
| .min(n) | string/number schema | For strings: minimum character length (inclusive). For numbers: minimum value (inclusive). Returns the schema for chaining. |
| .max(n) | string/number schema | For strings: maximum character length (inclusive). For numbers: maximum value (inclusive). Returns the schema for chaining. |
| .parse(value) | { ok, value } | { ok, issues } | Runtime parse. Returns { ok: true, value } with the typed value on success, or { ok: false, issues } with an array of validation errors on failure. |
| .jsonSchema() | object | Returns a JSON Schema object describing the schema shape. Used by the OpenAPI generator and compatible with any JSON Schema tooling. |
| Infer<T> | TypeScript utility | Extracts the TypeScript type from a schema: type MyType = Infer<typeof mySchema>. Import from @kynetra/fx. |