Claude Code for Valibot — Workflow Guide
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
-
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. -
Uses
z.object()for objects. Claude writesz.object({ name: z.string() }). Valibot usesobject({ name: string() })— noz.prefix, and functions are individually imported. -
Calls
.parse()for validation. Claude writesschema.parse(data). Valibot uses standalone functions:parse(schema, data)orsafeParse(schema, data)— the schema is a data structure, not a class with methods. -
Uses
z.inferfor types. Claude writestype User = z.infer<typeof UserSchema>. Valibot usestype User = InferOutput<typeof UserSchema>imported fromvalibot.
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
-
Import style affects bundle size. Claude uses
import * as v from 'valibot'and accessesv.string(),v.object(). This imports the entire library, defeating tree-shaking. Import functions individually:import { string, object, pipe } from 'valibot'. -
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. -
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.