Spec-driven validation + typing

Define a single operation with SchemaModel, generate validation, and keep your existing handler logic.

What you'll build

  • One command spec with explicit input/output models.
  • Validation + typing without rewriting your service layer.
  • Clear acceptance scenarios for regression safety.

1) Define the spec

Create src/contracts/contact-create.operation.ts:

src/contracts/contact-create.operation.ts
import { defineCommand } from "@contractspec/lib.contracts/operations";
import { SchemaModel, ScalarTypeEnum } from "@contractspec/lib.schema";

const ContactInput = new SchemaModel({
  name: "ContactInput",
  fields: {
    email: { type: ScalarTypeEnum.Email(), isOptional: false },
    firstName: { type: ScalarTypeEnum.NonEmptyString(), isOptional: false },
    lastName: { type: ScalarTypeEnum.NonEmptyString(), isOptional: false },
  },
});

const ContactOutput = new SchemaModel({
  name: "ContactOutput",
  fields: {
    id: { type: ScalarTypeEnum.String(), isOptional: false },
    email: { type: ScalarTypeEnum.Email(), isOptional: false },
    createdAt: { type: ScalarTypeEnum.DateTime(), isOptional: false },
  },
});

export const ContactCreateCommand = defineCommand({
  meta: {
    key: "contact.create",
    version: "1.0.0",
    description: "Create a CRM contact",
    owners: ["@sales"],
    tags: ["crm", "contacts"],
  },
  io: {
    input: ContactInput,
    output: ContactOutput,
  },
  policy: {
    auth: "user",
  },
  acceptance: {
    scenarios: [
      {
        key: "create-contact",
        given: ["User is authenticated"],
        when: ["ContactCreateCommand executes"],
        then: ["Contact is persisted", "Email is validated"],
      },
    ],
  },
});

2) Wire the handler

Keep your existing code. Just ensure the handler returns the output shape defined above.

src/handlers/contact-create.ts
import { ContactCreateCommand } from "@/contracts/contact-create.operation";
 
 export async function handleContactCreate(
   input: (typeof ContactCreateCommand)["io"]["input"],
   ctx: { userId: string }
 ) {
   // Your existing persistence logic
   return {
     id: "contact_123",
     email: input.email,
     createdAt: new Date().toISOString(),
   };
 }

3) Validate

spec-validation
contractspec validate src/contracts/contact-create.operation.ts

Expected output: Validation passed.

Example package

The CRM Pipeline example includes real specs, handlers, and presentations for contact + deal flows.

crm-example
# Build + validate the CRM pipeline example
cd packages/examples/crm-pipeline
bun install
bun run build
bun run validate

Need shared validation policies?

Studio lets teams enforce validation policies and review changes before they ship.

Join Studio waitlist