Claude Code for Valibot: Lightweight Runtime Schema Validation — Claude Skills 360 Blog
Blog / Backend / Claude Code for Valibot: Lightweight Runtime Schema Validation
Backend

Claude Code for Valibot: Lightweight Runtime Schema Validation

Published: February 21, 2027
Read time: 7 min read
By: Claude Skills 360

Valibot is a modular schema validation library — its tree-shakeable design means only the validators you import are bundled, typically 1–2 KB gzipped compared to Zod’s ~14 KB. object({}), string(), number(), array(), and union() build schemas composably. pipe(schema, ...actions) chains transforms and refinements — transform, minLength, email, regex, check. parse(schema, data) throws on validation failure; safeParse(schema, data) returns { success, output, issues }. InferOutput<typeof schema> extracts the TypeScript type. flatten(issues) formats errors into nested field paths for form libraries. Valibot integrates with @hookform/resolvers/valibot for React Hook Form, and @hono/valibot-validator for Hono route validation. Claude Code generates Valibot schema definitions, pipe transforms, form integrations, and the API validation middleware for TypeScript applications.

CLAUDE.md for Valibot

## Valibot Stack
- Version: valibot >= 0.42
- Schema: v.object, v.string, v.number, v.boolean, v.array, v.union, v.literal
- Transforms: v.pipe(schema, v.transform(fn)) — convert value after parse
- Refinements: v.pipe(schema, v.check(fn, "message")) — custom validate
- String: v.email(), v.url(), v.minLength(n), v.maxLength(n), v.regex(re), v.trim()
- Number: v.minValue(n), v.maxValue(n), v.integer(), v.multipleOf(n)
- Errors: v.safeParse(schema, data) → { success, output, issues }
- Types: v.InferOutput<typeof schema> — extract TypeScript type

Schema Definitions

// lib/schemas/order.ts — Valibot schemas
import * as v from "valibot"

// Reusable primitive schemas
const UUIDSchema = v.pipe(
  v.string(),
  v.uuid("Must be a valid UUID")
)

const PositiveIntSchema = v.pipe(
  v.number(),
  v.integer("Must be a whole number"),
  v.minValue(1, "Must be positive")
)

const CentsSchema = v.pipe(
  v.number(),
  v.integer("Cents must be a whole number"),
  v.minValue(0, "Amount cannot be negative")
)

const EmailSchema = v.pipe(
  v.string(),
  v.trim(),
  v.email("Must be a valid email address"),
  v.maxLength(254, "Email too long")
)

// Order item schema
export const OrderItemSchema = v.object({
  productId: UUIDSchema,
  name: v.pipe(v.string(), v.minLength(1), v.maxLength(200)),
  quantity: PositiveIntSchema,
  priceCents: CentsSchema,
})

// Create order input
export const CreateOrderInputSchema = v.object({
  items: v.pipe(
    v.array(OrderItemSchema),
    v.minLength(1, "Order must have at least one item"),
    v.maxLength(50, "Order cannot have more than 50 items")
  ),
  shippingAddress: v.object({
    line1: v.pipe(v.string(), v.minLength(1, "Required"), v.maxLength(100)),
    line2: v.optional(v.pipe(v.string(), v.maxLength(100))),
    city: v.pipe(v.string(), v.minLength(1, "Required"), v.maxLength(50)),
    postalCode: v.pipe(
      v.string(),
      v.regex(/^[A-Z0-9]{3,10}$/i, "Invalid postal code format")
    ),
    country: v.pipe(
      v.string(),
      v.length(2, "Must be a 2-letter country code"),
      v.toUpperCase()
    ),
  }),
  couponCode: v.optional(
    v.pipe(
      v.string(),
      v.trim(),
      v.toUpperCase(),
      v.regex(/^[A-Z0-9]{4,16}$/, "Invalid coupon code format")
    )
  ),
})

// Order status enum
export const OrderStatusSchema = v.picklist(
  ["pending", "processing", "shipped", "delivered", "cancelled"],
  "Invalid order status"
)

// Full order schema (from DB / API response)
export const OrderSchema = v.object({
  id: UUIDSchema,
  customerId: UUIDSchema,
  status: OrderStatusSchema,
  items: v.array(OrderItemSchema),
  totalCents: CentsSchema,
  couponCode: v.nullable(v.string()),
  createdAt: v.pipe(
    v.string(),
    v.isoTimestamp("Must be a valid ISO timestamp"),
    v.transform(s => new Date(s))
  ),
})

// Export TypeScript types
export type OrderItem = v.InferOutput<typeof OrderItemSchema>
export type CreateOrderInput = v.InferOutput<typeof CreateOrderInputSchema>
export type OrderStatus = v.InferOutput<typeof OrderStatusSchema>
export type Order = v.InferOutput<typeof OrderSchema>

Parse and SafeParse

// lib/validation.ts — parsing utilities
import * as v from "valibot"
import type { BaseSchema, BaseIssue } from "valibot"

// Strict parse — throws ValiError on failure
export function parseStrict<T>(schema: BaseSchema<unknown, T, BaseIssue<unknown>>, data: unknown): T {
  return v.parse(schema, data)
}

// Safe parse — returns result object
export function parseInput<T>(
  schema: BaseSchema<unknown, T, BaseIssue<unknown>>,
  data: unknown
): { success: true; data: T } | { success: false; errors: Record<string, string[]> } {
  const result = v.safeParse(schema, data)

  if (result.success) {
    return { success: true, data: result.output }
  }

  // Flatten issues into { fieldPath: ["error messages"] }
  const flat = v.flatten<typeof schema>(result.issues)
  const errors: Record<string, string[]> = {}

  // Root-level errors
  if (flat.root) {
    errors["_root"] = flat.root
  }

  // Nested field errors
  if (flat.nested) {
    for (const [path, messages] of Object.entries(flat.nested)) {
      if (messages) errors[path] = messages
    }
  }

  return { success: false, errors }
}

// Example usage
import { CreateOrderInputSchema } from "./schemas/order"

const result = parseInput(CreateOrderInputSchema, {
  items: [{ productId: "not-a-uuid", quantity: 0, priceCents: -1, name: "" }],
  shippingAddress: { line1: "123 Main St", city: "NYC", postalCode: "10001", country: "us" },
})

if (!result.success) {
  console.log(result.errors)
  // {
  //   "items.0.productId": ["Must be a valid UUID"],
  //   "items.0.quantity": ["Must be positive"],
  //   "items.0.priceCents": ["Amount cannot be negative"],
  //   "items.0.name": ["Too short"]
  // }
}

React Hook Form Integration

// components/CreateOrderForm.tsx — with @hookform/resolvers/valibot
import { useForm } from "react-hook-form"
import { valibotResolver } from "@hookform/resolvers/valibot"
import { CreateOrderInputSchema, type CreateOrderInput } from "@/lib/schemas/order"

export function CreateOrderForm({ onSuccess }: { onSuccess: (order: Order) => void }) {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<CreateOrderInput>({
    resolver: valibotResolver(CreateOrderInputSchema),
    defaultValues: {
      items: [],
      shippingAddress: { country: "US" },
    },
  })

  const onSubmit = async (data: CreateOrderInput) => {
    const response = await fetch("/api/orders", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    })

    if (!response.ok) {
      const err = await response.json()
      throw new Error(err.message)
    }

    onSuccess(await response.json())
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input
          {...register("shippingAddress.line1")}
          placeholder="Address line 1"
        />
        {errors.shippingAddress?.line1 && (
          <p className="error">{errors.shippingAddress.line1.message}</p>
        )}
      </div>

      <div>
        <input
          {...register("shippingAddress.city")}
          placeholder="City"
        />
        {errors.shippingAddress?.city && (
          <p className="error">{errors.shippingAddress.city.message}</p>
        )}
      </div>

      <div>
        <input
          {...register("shippingAddress.country")}
          placeholder="Country (2-letter code)"
          maxLength={2}
        />
        {errors.shippingAddress?.country && (
          <p className="error">{errors.shippingAddress.country.message}</p>
        )}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Placing order..." : "Place Order"}
      </button>
    </form>
  )
}

Hono Route Validation

// src/routes/orders.ts — Hono with valibot validator
import { Hono } from "hono"
import { vValidator } from "@hono/valibot-validator"
import * as v from "valibot"
import { CreateOrderInputSchema } from "../lib/schemas/order"

const orders = new Hono()

orders.post(
  "/",
  vValidator("json", CreateOrderInputSchema, (result, c) => {
    if (!result.success) {
      const flat = v.flatten(result.issues)
      return c.json(
        { error: "Validation failed", issues: flat.nested ?? flat.root ?? {} },
        422
      )
    }
  }),
  async (c) => {
    // result.output is typed as CreateOrderInput
    const input = c.req.valid("json")
    const totalCents = input.items.reduce(
      (sum, item) => sum + item.priceCents * item.quantity,
      0
    )
    const order = await createOrder({ ...input, totalCents })
    return c.json(order, 201)
  }
)

// Query param validation
const OrderQuerySchema = v.object({
  status: v.optional(v.picklist(["pending", "shipped", "delivered"])),
  limit: v.optional(
    v.pipe(
      v.string(),
      v.transform(Number),
      v.integer(),
      v.minValue(1),
      v.maxValue(100)
    )
  ),
})

orders.get(
  "/",
  vValidator("query", OrderQuerySchema),
  async (c) => {
    const { status, limit = 20 } = c.req.valid("query")
    const results = await listOrders({ status, limit })
    return c.json(results)
  }
)

export { orders }

Discriminated Union / Variant Schemas

// lib/schemas/events.ts — union schemas for event types
import * as v from "valibot"

const OrderCreatedSchema = v.object({
  type: v.literal("order.created"),
  orderId: v.pipe(v.string(), v.uuid()),
  customerId: v.pipe(v.string(), v.uuid()),
  totalCents: v.pipe(v.number(), v.integer(), v.minValue(0)),
})

const OrderShippedSchema = v.object({
  type: v.literal("order.shipped"),
  orderId: v.pipe(v.string(), v.uuid()),
  trackingNumber: v.string(),
  carrier: v.picklist(["FEDEX", "UPS", "USPS", "DHL"]),
})

const OrderCancelledSchema = v.object({
  type: v.literal("order.cancelled"),
  orderId: v.pipe(v.string(), v.uuid()),
  reason: v.optional(v.string()),
})

export const OrderEventSchema = v.variant("type", [
  OrderCreatedSchema,
  OrderShippedSchema,
  OrderCancelledSchema,
])

export type OrderEvent = v.InferOutput<typeof OrderEventSchema>

// Narrow with discriminant
function handleEvent(event: OrderEvent) {
  switch (event.type) {
    case "order.created":
      console.log(event.totalCents)  // TypeScript knows this exists
      break
    case "order.shipped":
      console.log(event.trackingNumber)  // TypeScript knows this exists
      break
  }
}

For the Zod alternative that has broader ecosystem adoption with more third-party integrations (tRPC, Drizzle, TanStack Form), larger bundle size but more mature API surface, and better error messages out of the box, the tRPC guide covers Zod-based input validation. For the ArkType alternative that provides even more expressive syntax with runtime type assertions similar to TypeScript syntax, the TypeScript-native validation patterns are available in the TypeScript utilities guide. The Claude Skills 360 bundle includes Valibot skill sets covering schema composition, transforms, and form integration. Start with the free tier to try Valibot schema generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free