Valibot - the last validation library you will ever need
I tested Valibot as viable alternative to yup approximatelly 3 years ago. Today I use it everywhere.
We were using yup when we switched the stack from PHP to fullstack JavaScript in 2018. We chosen this because it worked great with formik. We've been stuck with it ever since.
In 2022 when it seems that yup v1 might be released I started to thinking if we should upgrade all of our projects from v0.29.x or v0.32.x to v1 or we should switch the library altogether.
I've heard lot of great things about Zod at that time and I have also tried it on one of my personal projects. I liked the type inference and the overall API but then I've heard about this new validation library called Valibot. It was still shaping but I like the idea of small composable functions which are tree-shakable and all the powerful type inference which Zod offered at the time.
After few years of usage I can tell that it was the best decision because I stick to it ever since.
Since TypeScript only can help you in development phase not in runtime you still need to validate all the data which comes to your application - from anywhere. For instance you need to validate environment variables, user input, API endpoints and in most cases the database data.
These are just a few examples. You might want to use Valibot for generating your types meaning you don't need to create custom types by hand anymore. Most of the times you also want to validate the data not just type them so why won't you hit both birds with the same stone?
Examples
Validating environment variables
It is great idea to validate environment which your depends on.
import * as v from "valibot";
// describe what environment variables your application require
const envSchema = v.object({
STAGE: v.picklist(["stg", "prod", "dev"]), // if user pass anything else this will fail
DATA_TABLE_NAME: v.string(),
DATA_TABLE_GSI: v.config(v.string(), "DATA_TABLE_GSI is required"), // you can even pass your own error messages
IS_LOCAL: v.pipe(v.string(), v.transform(value => value === "true"), v.boolean()), // environment variables are always string but you can transform them easily so your application gets boolean
BASE_URI: v.optional(v.string(), "https://localhost:3000"), // you can even provide default values easily
});
// validate environment - if it does not fit the schema there is not much sense to continue execution
const env = v.parse(envSchema, process.env);
// use your variables - validated and transformed
if (env.IS_LOCAL) {
// do your stuff
}
Validating user input
Since standard-schema was introuced most of the form libraries could be used with Valibot or Zod or any other validation library which complies with standard-schema.
This means that you can use all the pipes, picklists, literals, variants, transformations, checks, async checks etc with your favourite form library as well. If not you can either wait or you should probably change the form library (if you can).
Or maybe your form library already uses Valibot internally.
Validating API endpoints
There are different solutions for validating API endpoints. You can use different technologies like GraphQL, REST, OpenAPI, JSON-RPC, JSON Schema and you can find tools which already validates your data.
There might be usecases where you need more than standard types which are provided by these technologies. You might need conditional validation etc. Here Valibot can help you.
For me, I like to validate Request data directly - without using anything fancy. Usually I write some wrapper for parsing/returning error boilerplate.
import * as v from "valibot";
const schema = v.object({
name: v.pipe(v.string(), v.minLength(3)),
emailAddress: v.pipe(v.string(), v.email()),
});
export async function updateUserHandler(request: Request): Promise<Response> {
// validate request body
const result = v.safeParse(schema, await request.json());
if (!result.success) {
// format the issues
const issues = v.flatten(result.issues);
return new Response(JSON.stringify(issues), {
status: 400,
});
}
// use validated data here
result.output.name;
result.output.emailAddress;
return new Response(JSON.stringify({ success: true }), {
status: 200,
});
}
Or if I use something fancy it will be tRPC which supports input validation with Valibot.
import { initTRPC } from "@trpc/server";
import { Context } from "./context";
import * as v from "valibot";
export const t = initTRPC.context<Context>().create();
export const appRouter = t.router({
updateUser: t.procedure
.input(
// here goes your valibot validation
v.object({
name: v.pipe(v.string(), v.minLength(3)),
emailAddress: v.pipe(v.string(), v.email()),
}),
)
.mutation(async ({ input }) => {
// your mutation
input.name;
input.emailAddres;
}),
});
Validating database data
On my projects I usually use DynamoDB database which is NoSQL key/value store. This means that the database itself does not enforce any schema except of primary keys and index keys. So you need to validate the data yourself.
This is great use-case for Valibot.
Since Valibot is written as a set of small composable functions this means that you can define generic schemas and reuse it for all of your models - for instance createdAt and updatedAt times might look like this:
import * as v from "valibot";
// you can reuse this within your models later
export const dateSchema = v.pipe(
v.string(),
v.transform((value) => new Date(value)),
v.date()
);
// example model
import * as v from "valibot";
const schema = v.object({
// in DynamoDB you can usually encode multiple values within your primary key, you can easily parse the data and validate them using transform -> validation in pipe
pk: v.pipe(
v.string(),
v.transform((value) => {
const [role, id] = value.split("#");
return { role, id };
}),
v.object({ id: v.string(), role: v.picklist(["user", "admin"]) }),
),
// here the sort key would look like this "BeeSolve#development#frontend devs" but you would get parsed data ready to use
sk: v.pipe(
v.string(),
v.transform((value) => {
const [company, department, team] = value.split("#");
return { company, department, team };
}),
v.object({
company: v.string(),
department: v.picklist(["finance", "development", "personal"]),
team: v.optional(v.string(), "no team assigned"),
}),
),
name: v.string(),
emailAddress: v.pipe(v.string(), v.email()),
createdAt: dateSchema,
updatedAt: dateSchema,
});
// great thing is that you can use the types as well
// use v.InferInput for data which will come from database or data which you want to write to database
type NewUser = v.InferInput<typeof schema>;
// use v.InferOutput for data which are validated/parsed eg. the model data in your application
type User = v.InferOutput<typeof schema>;
In DynamoDB you can mark record for deletion with time-to-live attribute which needs to be represented as UNIX timestamp in seconds. Since Date.getTime() in JavaScript gives you timestamp in milliseconds you can automate this by creating following schema:
import * as v from "valibot";
// you can use this when you get the data from database
export const expiresAtSchema = v.pipe(
v.number(),
v.transform((value) => new Date(value * 1000)),
v.date()
);
These are just few examples of how to use Valibot in real project. As I stated earlier you don't need other validation libraries anymore.
I hope you will help me to spread the word and give the Valibot try - if you are not using it already.
I recommed you to read the documentation and if you want to try it without installing it you can use the playground.. If you have a question or you are stuck you can ask for help at Discord - the team around Fabian is very helpful and responsive.
