Kynetra FX

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.

src/schemas/primitives.ts
import { fx, Infer } from '@kynetra/fx'
 
const Name = fx.string() // TypeScript: string
const Score = fx.number() // TypeScript: number
const Active = fx.boolean() // TypeScript: boolean
 
type Name = Infer<typeof Name> // string
type Score = Infer<typeof Score> // number
type Active = Infer<typeof Active> // boolean

Primitive 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.

src/schemas/constraints.ts
import { fx } from '@kynetra/fx'
 
// String length constraints
const Username = fx.string().min(3).max(30)
const Bio = fx.string().max(500)
 
// Number value constraints
const 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 schemas
const CreatePostInput = fx.object({
title: fx.string().min(1).max(200),
body: fx.string().min(10).max(10000),
views: fx.number().min(0),
})

Note

Constraints are validated in the order they are declared. If a value violates both .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.

src/schemas/enums.ts
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 object
const 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.

src/schemas/objects.ts
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

Nest schemas freely — 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.

src/schemas/arrays.ts
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.

src/schemas/optional.ts
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.

src/validate.ts
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

When you use 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.

src/introspect.ts
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.

src/routes/users.ts
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#

NameTypeDescription
.min(n)string/number schemaFor strings: minimum character length (inclusive). For numbers: minimum value (inclusive). Returns the schema for chaining.
.max(n)string/number schemaFor 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()objectReturns a JSON Schema object describing the schema shape. Used by the OpenAPI generator and compatible with any JSON Schema tooling.
Infer<T>TypeScript utilityExtracts the TypeScript type from a schema: type MyType = Infer<typeof mySchema>. Import from @kynetra/fx.
Schema Builder · Kynetra FX