Mastering Valibot in TypeScript: A Lightweight, Tree-shakable Schema Validation Guide

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

The Shift Toward Modular Schema Validation

For years, Zod has dominated the TypeScript ecosystem. It is reliable, developer-friendly, and integrates seamlessly with the type system. However, as developers obsess over performance metrics like Core Web Vitals, bundle size has become a primary concern.

Zod relies on a class-based, “fluent” API. This design makes tree-shaking difficult because every method is attached to a single, large object. Even if you only use a fraction of its features, you often end up shipping the entire 13.4kB (gzipped) library to your users.

Valibot offers a leaner path. Instead of chaining methods like z.string().email(), Valibot uses standalone, composable functions. This functional architecture allows modern bundlers like Vite, Webpack, or Rollup to perform surgical tree-shaking. If your project only validates strings, your production bundle only includes string logic. I have deployed this in production environments where every kilobyte matters, and the performance gains are immediate and measurable.

The Cost of Convenience: Valibot vs. Zod

Why is Valibot gaining so much traction? The answer lies in how it handles dependencies. Most traditional libraries use a “Fluent API” where methods are linked to a main object. While this looks clean in an IDE, it creates a rigid dependency graph that bundlers cannot easily break apart.

The Zod Approach (Method Chaining)

import { z } from 'zod';

const schema = z.string().min(5).email();
// The 'z' object is a monolith. 
// Your bundler cannot easily remove the methods you don't call.

The Valibot Approach (Functional & Modular)

import { string, minLength, email, pipe, parse } from 'valibot';

const schema = pipe(string(), minLength(5), email());
// Only these specific functions end up in your final JavaScript file.

By moving to independent functions, Valibot achieves a remarkably small footprint. In a typical real-world form, switching from Zod to Valibot can reduce your validation logic weight from ~13kB to less than 1kB. That is a 90% reduction in code size for the exact same functionality.

Pros and Cons: Is the Switch Worth It?

Choosing a library involves balancing trade-offs. While I recommend Valibot for performance-sensitive projects, you should consider both sides of the coin before migrating your entire codebase.

Pros

  • Minimal Bundle Size: It is currently one of the lightest validation libraries on the market.
  • Granular Tree-shaking: You only pay for the features you actually import.
  • Total Type Safety: It offers first-class TypeScript support with automatic type inference.
  • Easy Extensibility: Custom rules are just simple functions, making them easier to write than Zod plugins.

Cons

  • Evolving Ecosystem: It lacks the massive library of third-party integrations that Zod has built over the years.
  • Increased Verbosity: Some developers find the pipe() syntax more repetitive than Zod’s dot-chaining.
  • API Stability: As a newer library, the API has seen more frequent changes than mature alternatives.

Getting Started

Valibot works anywhere TypeScript runs, from Node.js and Bun to React and Vue. Installation is standard across all package managers:

npm install valibot
# or
pnpm add valibot

To keep things clean, I suggest creating a /schemas folder. This allows you to share validation logic between your frontend forms and your backend API routes without duplication.

Implementation Guide: From Basic to Advanced

Let’s look at how Valibot handles real-world data, starting with simple strings and moving to complex objects.

1. Basic Validation

To validate a single value, define your schema and use the parse function. Note that parse will throw a ValiError if the data is invalid.

import { string, minLength, pipe, parse } from 'valibot';

const passwordSchema = pipe(
  string(), 
  minLength(8, "Password must be at least 8 characters")
);

try {
  const result = parse(passwordSchema, "my-secure-password");
  console.log("Success:", result);
} catch (error) {
  console.error(error.message);
}

2. Safe Parsing for Production

I rarely use parse in production because catching errors with try/catch can be clunky. Instead, use safeParse. It returns a result object that tells you exactly what went wrong without stopping your code.

import { string, email, pipe, safeParse } from 'valibot';

const emailSchema = pipe(string(), email());
const result = safeParse(emailSchema, "not-an-email");

if (result.success) {
  console.log("Data is valid:", result.output);
} else {
  // Access detailed error issues here
  console.log("Validation failed:", result.issues);
}

3. Validating Complex Objects

Valibot shines when handling nested API responses. You can even extract TypeScript types directly from your schemas using InferOutput.

import { object, string, number, pipe, minLength, email, type InferOutput } from 'valibot';

const UserProfileSchema = object({
  username: pipe(string(), minLength(3)),
  age: number(),
  email: pipe(string(), email())
});

type UserProfile = InferOutput<typeof UserProfileSchema>;

const data = {
  username: "dev_user",
  age: 30,
  email: "[email protected]"
};

4. Integration with React Hook Form

Most frontend developers use Valibot within a form library. You can connect it to React Hook Form using the official resolver package.

import { useForm } from 'react-hook-form';
import { valibotResolver } from '@hookform/resolvers/valibot';

function RegistrationForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: valibotResolver(UserProfileSchema)
  });

  const onSubmit = (data) => console.log("Form Data:", data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("username")} />
      {errors.username && <p>{errors.username.message}</p>}
      <button type="submit">Register</button>
    </form>
  );
}

Final Thoughts on Performance

The strength of Valibot is its simplicity. When I migrated a medium-sized project from Zod, the “First Load JS” metric in Lighthouse improved by several points. While Zod’s 13KB might seem small, it adds up when combined with other libraries. Valibot’s ability to scale down to 1KB makes it the superior choice for modern, performance-first web development. If you are starting a new project today, choose the library that respects your user’s bandwidth.

Share: