# @wrnexus/validation > One fluent schema, validated on the server (API bodies, env vars) and mirrored to an eval-free browser validator for forms. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. ## Overview Define a schema once with the fluent `v` builder, then reuse it in three places: `.parse()` runs server-side and returns coerced values plus per-field errors; `.describe()` emits a plain-JSON `SchemaDescriptor` that the browser runtime interprets (no `eval`, no bundled validator); and helpers like `parseBody` and `parseEnv` wire schemas straight into API routes and startup config. The server rule logic (`applyRule`/`checkField`) and the client runtime (`VALIDATE_RUNTIME`) mirror each other exactly, so a form validates identically in both places. Schemas are conventionally kept in `app/schemas/`. ## Installation ```bash bun add @wrnexus/validation ``` > Private package — the machine must be authenticated to the `wrnexus` npm org > (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported). ## API ### The `v` builder ```ts import { v } from "@wrnexus/validation"; ``` | Factory | Returns | Field methods | | ------------------ | --------------- | ------------------------------------------------------------------------------------------------------------------- | | `v.string()` | `StringSchema` | `email()`, `url()`, `uuid()`, `date()`, `length(n)`, `oneOf(string[])`, `pattern(re)`, `trim()`, `min(n)`, `max(n)` | | `v.number()` | `NumberSchema` | `integer()`, `positive()`, `oneOf(number[])`, `min(n)`, `max(n)` | | `v.boolean()` | `BooleanSchema` | (base methods only) | | `v.object(fields)` | `ObjectSchema` | `parse(input)`, `describe()` | Every field schema is chainable and shares these base methods: - `min(n, message?)` / `max(n, message?)` — for strings, bounds the length; for numbers, bounds the value. - `optional()` — an empty/missing value passes instead of erroring `"Required"`. - `label(text)` — human label carried into the descriptor. - `default(value)` — value substituted when the field is absent (implies `optional`). - `refine(fn, message?)` — **server-only** predicate. `fn` returns `true` (ok), `false` (use `message`), or a `string` (that error). Not serialized to the client. Each string rule accepts an optional trailing `message` to override the default error text. ### `ObjectSchema` ```ts schema.parse(input: unknown): ParseResult schema.describe(): SchemaDescriptor ``` `parse` coerces each field (strings stay strings, `v.number()` runs `Number()`, `v.boolean()` treats `true` / `"true"` / `"on"` as true), applies its rules and refinements, fills in `default()` values, and returns: ```ts interface ParseResult> { ok: boolean; // true when errors is empty value: T; // coerced values (present pass or fail) errors: Record; // field name → first failing message } ``` `describe()` returns the JSON bridge for the client: ```ts interface SchemaDescriptor { type: "object"; fields: Record; } interface FieldDescriptor { type: "string" | "number" | "boolean"; optional?: boolean; label?: string; trim?: boolean; // strings only rules: RuleDescriptor[]; } ``` ### Rules and coercion `RuleDescriptor` is a discriminated union of the serializable rules — `min`, `max`, `length`, `email`, `url`, `uuid`, `date`, `oneOf`, `pattern`, `integer`. Two exported functions apply them and are shared by the server (the client runtime reimplements the same logic): - `applyRule(type, rule, value): string | null` — validate one already-coerced value against one rule. - `checkField(desc, raw): { value, error }` — coerce and validate one field. Empty input (`undefined`/`null`/`""`) is `"Required"` unless `optional`. Strings with `trim` are trimmed first. Numbers that fail `Number()` yield `"Must be a number"`. Notes on specific rules: `email`/`url`/`uuid` test built-in regexes; `date` uses `Date.parse`; `pattern` reconstructs a `RegExp` from its `source`/`flags` and passes silently if the pattern is invalid; `integer` requires `Number.isInteger`; `positive()` is implemented as `min(Number.MIN_VALUE)`. ### API helpers ```ts invalid(errors: Record): Response // ready 400 { ok:false, errors } parseBody(schema, req): Promise<{ ok: true; value: T } | { ok: false; response: Response }> ``` `parseBody` reads the request body from JSON, `application/x-www-form-urlencoded`, or `multipart/form-data`, validates it, and on failure hands back a ready 400 `Response`. ### Environment config ```ts parseEnv(schema: ObjectSchema, source?): T ``` Validates env vars (from `Bun.env`, falling back to `process.env`) against a schema and coerces them (`PORT` → number, `DEBUG` → boolean). On any problem it throws **one** error listing every offending variable, so misconfiguration fails fast at startup. ### Client runtime (from `runtime.ts`) ```ts renderSchemasScript(descriptors: Record): string VALIDATE_RUNTIME: string ``` - `renderSchemasScript` produces `window.__wireSchemas = { name: descriptor, … };` to inline in the page. - `VALIDATE_RUNTIME` is a self-contained, eval-free IIFE string. Injected as a ` `; // render a
with [data-error="email"] etc. ``` ## Requirements / Notes - **Bun-only.** `parseEnv` reads `Bun.env` (falling back to `process.env`); `parseBody` and `invalid` use the Web `Request`/`Response` APIs that back `Bun.serve`. - Refinements (`refine`) run only server-side and are never serialized — client and server agree on every other rule because both interpret the same `RuleDescriptor` list. - No runtime dependencies. Ships as TypeScript source (`src/index.ts`) executed directly by Bun. - Pairs with the WrNexus server (`@wrnexus/core`) for route handlers and the SSR layer that injects `renderSchemasScript` / `VALIDATE_RUNTIME`.