When building a web app, you often need to make sure the data you’re working with is valid—whether it’s coming from a form, an API, or a file. That’s where Valibot comes in.
Valibot is a small and fast JavaScript/TypeScript library that helps you define rules for your data and check if it meets them. Even though it’s lightweight, it’s packed with features like custom validation, schema building, and great TypeScript support.
This article shows you how to build a validation service for your JavaScript app using Valibot.
Prerequisites
Before you start, make sure you have Node.js and npm installed on your computer. You should also be familiar with basic JavaScript/TypeScript and understand the importance of data validation for your applications.
Getting started with Valibot
To try out what you'll learn, create a new project. Open your terminal and run these commands:
Now install Valibot:
Create a file called index.js in your project folder and add this code:
This example defines a simple schema using v.object() with two fields: name (a string) and age (a number).
Then, v.parse() checks an object against the schema. If the object matches, it returns the validated data.
Save this file and run it with the following command:
You should see this output:
You've now created your first validation schema and validated data against it. The validated data is returned as-is when it matches the schema.
Handling validation errors
In the last example, the data matched the schema and was accepted. But in real-world apps, users often send invalid data. Let's see how Valibot handles these cases.
Update your index.js with this code:
This tries to validate an object where age is a string instead of a number.
Since the data doesn't match the schema, Valibot throws a ValiError that you can catch and log.
Save the file and run it:
You should see an error like:
When validation fails, Valibot throws an error with a clear message.
The issues property gives more structured details about what went wrong, including the expected type, the received value, and where in the data the problem happened.
Adding validation rules
So far, you've validated that values are the correct type (string, number), but real applications need more detailed validation. For example, you might want to ensure:
- A username is at least 3 characters long
- An age is a positive number
- An email address has the correct format
Valibot lets you add these rules to your schemas using the pipe function. Let's modify our previous example:
Here, you've enhanced our previous personSchema by adding the v.pipe() function with a length validator. The pipe function lets you chain validators together:
v.string()- First check that the value is a stringv.minLength(3)- Then checks that the string is at least 3 characters long
Save this file and run it with:
You should see this output:
Now, let's add more validation rules to create a complete user schema:
You've expanded the schema to include:
- A maximum length for the name
- Email validation using the built-in email() validator
- Age validation to ensure users are at least 18 years old
You're also demonstrating how to display all validation errors simultaneously, which is particularly helpful for form validation in real-world applications.
Save the updated file and rerun it:
You should see this output:
Valibot collects all validation errors, not just the first one it encounters. This is helpful when validating form data, as it allows you to show users all the problems they need to fix at once.
Creating a validation service
Currently, all our validation resides within the main file. To keep things organized and scalable, it’s a good idea to move validation into its own module. This way, your schemas and validation functions remain separate from the rest of your app logic, making them easier to manage.
Let's create a separate file for our validation logic. First, make a new file called validator.js with the following:
The validation service wraps the schema and parsing logic in a function that returns a consistent result structure. When validation succeeds, it returns an object with success: true and the validated data. When validation fails, it returns success: false and a more user-friendly format for the errors.
Notice how we transform the error issues into a simpler structure. Instead of passing along the entire Valibot error object, we extract just the field path and error message—the parts your application code actually needs.
Now update your index.js to use this validation service:
With this approach, your main code doesn't need to handle try/catch blocks or directly interact with Valibot's error format. It simply calls the validation function and checks the success property to determine what to do next. This pattern works well for validating form data, API requests, and other user inputs.
Save both files and run the main file:
You should see this output:
Separating validation logic makes real-world apps easier to manage. You'll often need to validate the same data in forms, APIs, and file imports. Centralizing validation ensures consistent rules everywhere and makes updates simpler when requirements change.
Using Valibot with TypeScript
In this section, you'll use Valibot with TypeScript to generate types directly from your schemas. This lets you validate data at runtime while getting full type safety during development.
Let's convert the project to TypeScript. First, install TypeScript and related tools:
Then initialize a basic TypeScript configuration:
Now, let's convert the validator to TypeScript. Rename your existing validator.js file to validator.ts so we can start using TypeScript features:
The key addition here is type Person = v.InferOutput<typeof personSchema>, which automatically generates a TypeScript type from our schema. This means we don't have to define a type that matches our schema manually - Valibot does it for us.
Now, let's rename the existing index.js file to index.ts so it can use the typed validator:
Run the TypeScript file with tsx:
You should see output similar to:
What makes this approach powerful is that TypeScript now knows precisely what structure a valid Person has. When you access properties on validResult.data, you get complete type checking, autocompletion, and compiler errors if you try to access properties that don't exist.
The type safety extends to your entire codebase. For example, if you pass the validated data to another function, TypeScript will ensure that function uses the data correctly. This helps catch errors at compile time rather than runtime.
Final thoughts
You’ve now seen how to use Valibot for clean, consistent data validation, from basic JavaScript to full TypeScript integration.
This setup works well for forms, APIs, and more. To explore advanced features like custom validators or async parsing, check out the official Valibot docs.