Skip to content

Otok Docs

Validation#

@kamod-ch/otok-validation validates route action inputs using Standard Schema V1 — compatible with Zod, Valibot, and ArkType without forcing a schema library in Otok core.

defineAction with schema#

import { defineAction } from "@kamod-ch/otok-validation/loader";
import { contactSchema } from "./schemas/contact";

export const action = defineAction({
  schema: contactSchema,
  handler: async ({ input, db }) => {
    return db.insertInto("contacts").values(input).executeTakeFirstOrThrow();
  },
});

Input is parsed automatically:

  • HTML forms → FormData (progressive enhancement)
  • JSON APIs → Content-Type: application/json
  • Route params → use parseParams(params, schema) in loaders

Error display#

Validation failures throw Otok validationError() with a consistent shape:

{
  status: 400,
  message: "Validation failed",
  fieldErrors: {
    email: ["Invalid email address"],
    name: ["Name is required"],
  },
  values: { email: "bad", name: "" },  // safe redisplay
}

In your page component:

export default function NewContact({ actionData }) {
  const failure = actionData;
  const values = failure?.values ?? {};

  return (
    <form method="post">
      <input name="email" value={values.email ?? ""} aria-invalid={Boolean(failure?.fieldErrors?.email)} />
      {failure?.fieldErrors?.email?.map((e) => (
        <p role="alert">{e}</p>
      ))}
    </form>
  );
}

Field errors map to form fields. Form-level errors (no path) appear in formErrors.

Schema libraries#

Library Usage
Zod 4+ Pass schema directly
Valibot Pass schema directly (Standard Schema native)
ArkType Wrap with fromArkType() from @kamod-ch/otok-validation/adapters/arktype
Zod 3 Supported via legacy safeParse interface

Client-side validation#

Same contract on server and client:

import { safeValidate } from "@kamod-ch/otok-validation/client";

const result = await safeValidate(contactSchema, formValues);
if (!result.success) showErrors(result.fieldErrors);

Options#

parseFormData(formData, schema, {
  status: 422, // HTTP status (400 or 422)
  values: true, // include redisplay values (default: true for forms)
  stripUnknown: true, // remove unknown fields before validation
  arrays: ["tags"], // repeated form fields → array
  checkboxes: ["active"], // checkbox normalization
});

Example#

See examples/contacts-crud for validation integrated with Kysely CRUD routes.