Claude Code for Valibot — Workflow Guide

Written by Michael Lip · Solo founder of Zovo · $400K+ on Upwork · 100% JSS Join 50+ builders · More at zovo.one

The Setup

You are using Valibot for runtime validation, the tree-shakeable alternative to Zod that produces 90% smaller bundle sizes. Valibot uses a modular pipe-based API instead of method chaining, meaning you only bundle the validation functions you actually use. Claude Code writes Zod code when asked for validation, missing Valibot’s completely different API.

What Claude Code Gets Wrong By Default

  1. Writes Zod method chains. Claude generates z.string().min(1).email(). Valibot uses function composition: pipe(string(), minLength(1), email()). Each validator is a separate import that tree-shakes independently.

  2. Uses z.object() for objects. Claude writes z.object({ name: z.string() }). Valibot uses object({ name: string() }) — no z. prefix, and functions are individually imported.

  3. Calls .parse() for validation. Claude writes schema.parse(data). Valibot uses standalone functions: parse(schema, data) or safeParse(schema, data) — the schema is a data structure, not a class with methods.

  4. Uses z.infer for types. Claude writes type User = z.infer<typeof UserSchema>. Valibot uses type User = InferOutput<typeof UserSchema> imported from valibot.

The CLAUDE.md Configuration

# Valibot Validation Project

## Validation
- Library: Valibot (modular, tree-shakeable validation)
- API: Function composition with pipe(), NOT method chaining
- Import: individual functions from 'valibot'

## Valibot Rules
- Schemas: object({ name: string() }) not z.object(...)
- Pipe for refinements: pipe(string(), minLength(1), email())
- Validate: parse(schema, data) or safeParse(schema, data)
- Type inference: InferOutput<typeof schema>
- Optional: optional(string()) not string().optional()
- Nullable: nullable(string())
- Arrays: array(string()) not string().array()
- Union: union([string(), number()])
- Transform: transform(input, (val) => val.trim())

## Conventions
- Import validators individually (enables tree-shaking)
- Schemas in src/schemas/ directory
- One schema per domain concern
- Use safeParse for form validation (returns issues array)
- Use parse for API validation (throws ValiError)
- Never import from 'zod' — this project uses Valibot
- Pipe order matters: validators run left to right

Workflow Example

You want to validate a contact form submission. Prompt Claude Code:

“Create a Valibot schema for a contact form with name (required, 2-100 chars), email (required, valid email), subject (optional, max 200 chars), and message (required, 10-5000 chars). Validate the form data and return typed errors.”

Claude Code should create schemas with pipe() composition: pipe(string(), minLength(2), maxLength(100)) for name, use safeParse(contactSchema, formData) for validation, and map result.issues to field-level error messages using issue.path to identify which field failed.

Common Pitfalls

  1. Import style affects bundle size. Claude uses import * as v from 'valibot' and accesses v.string(), v.object(). This imports the entire library, defeating tree-shaking. Import functions individually: import { string, object, pipe } from 'valibot'.

  2. Pipe ordering errors. Claude puts transform() before validators in the pipe. Valibot processes pipes left to right — put type validators first, then refinements, then transforms. A transform before a type check can cause runtime errors on invalid input.

  3. Error message customization location. Claude tries to add error messages as string arguments to validators. In Valibot, custom messages go as the last argument to the validator function: minLength(1, 'Name is required'), not as a separate config object.