Schema First TypeScript Design with Valibot
Yesterday’s post was about avoiding any. Today I want to talk about the next step: making your types actually work for you.
Here’s the problem. TypeScript gives you compile-time safety. Beautiful, precise types that catch bugs before you run anything. Then the compiler strips them all away. At runtime, that carefully typed User object is just a plain JavaScript object—and nothing stops malformed data from sneaking in through an API boundary.
Schema-first design flips the approach: define schemas that validate at runtime, then derive your TypeScript types from them. One source of truth, two guarantees.
Why schema-first beats types-first #
Most TypeScript projects start types-first. You write an interface, then later bolt on validation with if checks or a validation library. The type and the validation logic drift apart over time because—let’s be honest—nobody maintains them together.
Schema-first inverts that. The schema is the source of truth. Types flow from it:
import * as v from 'valibot';
const UserSchema = v.object({
id: v.string(),
email: v.pipe(v.string(), v.email()),
age: v.pipe(v.number(), v.minValue(0)),
});
// Type is inferred---no separate interface needed
type User = v.InferOutput<typeof UserSchema>;Change the schema, and both the validation behavior and the TypeScript type update together. No drift. No “the type says string but the validator allows null” surprises.
Enter Valibot #
Valibot launched in July 2023, supervised by Misko Hevery (Angular, Qwik creator) and Ryan Carniato (SolidJS creator). The pitch: a modular, tree-shakable schema validation library that can cut bundle size by up to 95% compared to Zod.
That number sounds like marketing fluff—but the architectural reason is real. Zod uses a class-based API where every schema inherits a base class with all methods attached. Even if you only use z.string(), the bundler can’t tree-shake the methods you didn’t call.
Valibot uses a function-based API instead. Each validation capability is an independent function import:
// Only these specific functions end up in your bundle
import { object, string, number, pipe, email, minValue } from 'valibot';For server-side code where bundle size doesn’t matter, the difference is academic. For client-side apps, edge functions, or serverless handlers where every kilobyte counts—it’s significant.
How it compares #
Zod is the incumbent. Massive ecosystem, tRPC integration, widespread adoption. If you need community support and battle-tested edge cases, Zod wins. The API is also excellent—.parse(), .safeParse(), chaining methods. Hard to complain.
But Zod’s bundle size has been a real concern for projects targeting lean deployments. Yup is lighter but has a different API philosophy (schema definition is more verbose, and the async-first approach feels heavy for synchronous use cases). io-ts is type-theoretically elegant but the learning curve is steep.
Valibot threads the needle: Zod-like ergonomics with a modular architecture. The tradeoff is maturity. Valibot is pre-1.0 as I write this. The API surface is still shifting. I wouldn’t recommend it for a large production codebase today—but I’m evaluating it for new projects at DreamFlare where we can absorb some API churn.
The schema-first workflow in practice #
At DreamFlare our API boundary validation follows this pattern:
Define schemas in a shared schemas/ directory. Infer types from those schemas. Use the schemas to validate incoming request bodies, query parameters, and webhook payloads. Use the inferred types in application code.
The shared schemas become the contract between frontend and backend. When someone changes a field, the schema update propagates type errors to every consumer. It’s the same idea as GraphQL’s schema-first approach, but for internal TypeScript boundaries.
One thing I like about Valibot specifically: pipe-based composition reads well. Instead of method chaining, you compose validation steps explicitly:
const PortSchema = v.pipe(
v.number(),
v.integer(),
v.minValue(1),
v.maxValue(65535)
);Each step is a discrete function. Easy to test individually, easy to compose, and—critically—easy for the bundler to eliminate if unused elsewhere.
Should you switch from Zod? #
Probably not yet. Not for existing projects. Zod’s ecosystem is too valuable, and the migration cost isn’t justified by bundle savings alone for most teams.
But if you’re starting a new project, especially one where bundle size matters (client-side validation, edge workers, serverless), give Valibot a serious look. The schema-first pattern works identically with either library; you’re just choosing the engine underneath.
My bet is that by this time next year, Valibot’s ecosystem will be mature enough for broader adoption. The fundamentals are sound, the maintainers are credible, and the modular architecture is the right call for where JavaScript tooling is headed.
Define your schemas. Derive your types. Validate at runtime. Whichever library you pick, that workflow is the one worth committing to.