OSS-first docs

These docs teach the open system first: contracts, generated surfaces, runtimes, governance, and incremental adoption. Studio shows up as the operating layer on top, not as the source of truth.

Build a contract-driven form

Define form data, fields, layout, policy, and submit behavior once, then let your app render from the same ContractSpec surface.

What you'll build

  • A schema-backed FormSpec for a lead capture form.

  • A FormRegistry that exposes the latest version by stable key.

  • A React renderer that keeps UI, validation, and policy aligned.

1) Define the form spec

Create

src/contracts/forms/lead-capture.form.ts

:

src/contracts/forms/lead-capture.form.ts
import {
  defineFormSpec,
  FormRegistry,
  responsiveFormColumns,
} from "@contractspec/lib.contracts-spec/forms";
import { fromZod } from "@contractspec/lib.schema";
import { z } from "zod";

const LeadCaptureModel = fromZod(
  z.object({
    fullName: z.string().min(1),
    email: z.string().email(),
    company: z.string().min(1),
    intent: z.enum(["demo", "pricing", "pilot"]),
    notes: z.string().max(1000).optional(),
    newsletter: z.boolean().optional(),
  }),
  { name: "LeadCaptureModel" },
);

export const LeadCaptureForm = defineFormSpec({
  meta: {
    key: "marketing.lead.form",
    version: "1.0.0",
    title: "Lead capture form",
    description: "Collect qualified OSS and Studio leads.",
    domain: "marketing",
    owners: ["@growth"],
    tags: ["forms", "leads"],
    stability: "experimental",
  },
  model: LeadCaptureModel,
  layout: {
    columns: responsiveFormColumns(2),
    gap: "md",
    flow: {
      kind: "sections",
      sections: [
        { key: "identity", titleI18n: "Who you are", fieldNames: ["fullName", "email", "company"] },
        { key: "request", titleI18n: "What you need", fieldNames: ["intent", "notes", "newsletter"] },
      ],
    },
  },
  fields: [
    { kind: "text", name: "fullName", labelI18n: "Full name", required: true, autoComplete: "name" },
    { kind: "email", name: "email", labelI18n: "Work email", required: true, autoComplete: "email" },
    { kind: "text", name: "company", labelI18n: "Company", required: true, autoComplete: "organization" },
    {
      kind: "select",
      name: "intent",
      labelI18n: "Primary goal",
      required: true,
      options: [
        { labelI18n: "Book a demo", value: "demo" },
        { labelI18n: "Understand pricing", value: "pricing" },
        { labelI18n: "Plan a pilot", value: "pilot" },
      ],
    },
    { kind: "textarea", name: "notes", labelI18n: "Context", rows: 4, layout: { colSpan: "full" } },
    { kind: "checkbox", name: "newsletter", labelI18n: "Send product updates" },
  ],
  actions: [{ key: "submit", labelI18n: "Send request", op: { name: "lead.create", version: "1.0.0" } }],
  policy: { flags: ["lead-capture"], pii: ["fullName", "email", "company", "notes"] },
  renderHints: { ui: "shadcn", form: "react-hook-form" },
});

export const formRegistry = new FormRegistry().register(LeadCaptureForm);

Need every form field?

Start from the Form Showcase example or the form template catalog. It is focused only on forms and covers field kinds, section layouts, step layouts, arrays, groups, conditionals, and validation hints.

form-showcase-template
bun add @contractspec/example.form-showcase

# Full form-only example docs
open https://www.contractspec.io/docs/examples/form-showcase

# Template catalog filtered to forms
open https://www.contractspec.io/templates?tag=forms

# Sandbox spec preview
open https://www.contractspec.io/sandbox?template=form-showcase

2) Render from the contract

Use the shared renderer, or provide your own driver if your app uses a different component library.

src/components/LeadCapturePanel.tsx
"use client";

import { formRenderer } from "@contractspec/lib.design-system/renderers/form-contract";
import { LeadCaptureForm } from "../contracts/forms/lead-capture.form";

export function LeadCapturePanel() {
  return formRenderer.render(LeadCaptureForm, {
    defaultValues: { newsletter: true },
    overrides: {
      onSubmitOverride: async (values, actionKey) => {
        await fetch("/api/leads", {
          method: "POST",
          headers: { "content-type": "application/json" },
          body: JSON.stringify({ actionKey, values }),
        });
      },
    },
  });
}

3) Validate and evolve

validate-form
contractspec validate src/contracts/forms/lead-capture.form.ts

Expected output:

Validation passed

. When the form becomes public contract surface, version changes deliberately instead of editing field meaning in place.

Repo tutorial

The repository guide adds the longer implementation checklist, custom renderer notes, and rollout rules.

repo-guide
open docs/tutorials/contract-driven-forms.md

Need governed form changes?

Studio can connect form edits to customer evidence, approval packets, and rollout checks before teams regenerate public surfaces.

See what Studio adds
OSS docsbuildStart with OSS. Adopt Studio when you want the operating layer.

Why ContractSpec

Keep educational and comparison content reachable without letting it define the primary OSS learning path.