Forms
Author questionnaires with defineForm, field rules, showWhen, and $Infer.
Forms are TypeScript (defineForm) or rows in your database (saveForm). Code-authored forms feed $Infer. Database forms do not.
Authoring Forms in TypeScript
We recommend defining each questionnaire in a dedicated file under lib/forms/:
import { defineForm } from "@dimah-form/server";
export const onboardingForm = defineForm({
title: "Employee Onboarding",
slug: "onboarding",
description: "Complete your profile and equipment preferences.",
status: "active",
fields: [
{
id: "fullName",
type: "text",
label: "Full Name",
required: true,
minLength: 2,
maxLength: 100,
},
{
id: "department",
type: "select",
label: "Department",
required: true,
options: [
{ value: "engineering", label: "Engineering" },
{ value: "design", label: "Product Design" },
{ value: "marketing", label: "Marketing" },
],
},
{
id: "githubHandle",
type: "text",
label: "GitHub Username",
required: true,
showWhen: {
field: "department",
equals: "engineering",
},
},
{
id: "portfolioUrl",
type: "text",
label: "Portfolio Link",
showWhen: {
field: "department",
equals: "design",
},
},
{
id: "newsletter",
type: "boolean",
label: "Subscribe to company updates",
defaultValue: true,
},
],
});Export your forms from a central index:
import { onboardingForm } from "./onboarding";
export const forms = {
onboarding: onboardingForm,
};Pass the forms dictionary into dimahForm():
import { dimahForm, memoryAdapter } from "@dimah-form/server";
import { forms } from "@/lib/forms";
export const form = dimahForm({
database: memoryAdapter(),
forms,
});
export type Form = typeof form;Form Properties
| Property | Type | Default | Description |
|---|---|---|---|
title | string | — | Human-readable title of the questionnaire. |
slug | string | Catalog key | Unique URL-friendly slug. getForm and startResponse accept id or slug. |
description | string | undefined | Optional subtitle or instructions for respondents. |
status | "active" | "draft" | "archived" | "active" | Only "active" forms can start new response sessions. |
fields | FormField[] | [] | Array of field definitions. |
meta | Record<string, unknown> | {} | Custom JSON dictionary for UI layout hints, icons, or category tags. |
Form Status Lifecycle
active: Form is published and accepts new responses viastartResponse.draft: Form is still being designed. Starting a response returns409 FORM_INACTIVE.archived: Form is deprecated. Existing submitted responses remain readable, but new responses are blocked.
Field Anatomy
Each item in fields requires an id, a type, and an optional label:
{
id: "workEmail",
type: "email",
label: "Work Email Address",
required: true,
defaultValue: "user@example.com",
meta: {
placeholder: "you@company.com",
tooltip: "We will send your onboarding packet here.",
},
}Common Field Attributes
id: Unique string identifier within the form. Serves as the key in the answers dictionary.type: Built-in type ("text","number","boolean","select","multiSelect","email","date") or a custom type registered viadefineFieldType.label: Display label for UI components (retrieved viafieldLabel(field)).required: Iftrue, the field must be non-empty when submitting the form (if visible).defaultValue: Seeded into the answers map upon callingstartResponse.showWhen: Conditional visibility rules.meta: Arbitrary metadata for UI customization (e.g., placeholder, step number, layout width).
Conditional Visibility (showWhen)
Fields can dynamically show or hide based on the current answers. When a field is hidden, it is excluded from visibleFields and its answer is stripped before final submission.
1. Simple Conditions
// Check exact value match
showWhen: { field: "role", equals: "manager" }
// Check inequality
showWhen: { field: "plan", notEquals: "free" }
// Check if a multiSelect array contains a specific option
showWhen: { field: "techStack", includes: "typescript" }2. Compound Conditions (all / any)
// AND condition (all must match)
showWhen: {
all: [
{ field: "experience", equals: "senior" },
{ field: "location", equals: "remote" },
],
}
// OR condition (at least one must match)
showWhen: {
any: [
{ field: "department", equals: "engineering" },
{ field: "department", equals: "product" },
],
}Code-Authored vs Database Forms
| Feature | Code-Authored Forms (forms: {}) | Dynamic Database Forms (saveForm()) |
|---|---|---|
| Declaration | TypeScript files via defineForm | Admin UI / CMS via saveForm API endpoint |
| Type Inference | Full compile-time $Infer typing | Record<string, unknown> |
| Version Control | Tracked in Git alongside application code | Stored in the questionnaire database table |
| Overwrite Protection | Protected from accidental API deletion / updates | Can be modified or deleted via API routes |
End-to-End Type Inference ($Infer)
When you export export type Form = typeof form, dimah-form automatically derives the exact shape of your answers:
import type { Form } from "@/lib/form";
type OnboardingAnswers = Form["$Infer"]["answers"]["onboarding"];
// TypeScript automatically infers:
// type OnboardingAnswers = {
// fullName: string;
// department: "engineering" | "design" | "marketing";
// githubHandle?: string;
// portfolioUrl?: string;
// newsletter?: boolean;
// }Use this type in your React client:
const q = useFormResponse<Form["$Infer"]["answers"]["onboarding"]>({
snapshot: formSnapshot,
});