Schema validation is a must-have for any production-ready app, as any data from users or other external sources needs to conform to a predefined structure or format to maintain data integrity and prevent any unexpected behaviors in our applications.
Typically, developers have to come up with validation for input data when a user submits a form to a website or for payload sent to an API via an HTTP request. However, writing this validation logic manually can be repetitive and time-consuming, which isn’t good for developer productivity.
Fortunately, libraries for common development tasks have hardly been a problem within the JavaScript community, and schema validation is no exception.
In this article, we’ll compare VineJS and Zod by evaluating their validation capabilities, performance, integration with tools, and ecosystem. By the end, you’ll see that while VineJS excels with performance, Zod’s versatility and strong TypeScript integration make it a more well-rounded choice for most projects.
VineJS is a modern JavaScript/TypeScript schema validation library designed to be lightweight, easy to use, and highly performant.
The project originated from the AdonisJS validator codebase and has been upgraded and released as a standalone library. VineJS was built for use in Node.js server-side environments, especially in scenarios like validating incoming requests to an API to make sure the payload is of the expected format before further processing.
Some of the key features of VineJS include:
In the next section, we’ll see how some of these features come into play.
Let’s look into some of VineJS’s schema validation capabilities.
When working with user inputs or data from external sources, validating basic data types like strings, numbers, and booleans is often the first step. VineJS simplifies this process with its intuitive API.
For example, let’s validate a user’s age:
import vine, { errors } from "@vinejs/vine"; // NOTE: VineJS is ESM only const ageSchema = vine.number().min(18).max(30); try { const output = await vine.validate({ schema: ageSchema, data: 21 }); console.log(output); } catch (error) { if (error instanceof errors.E_VALIDATION_ERROR) { console.log("validation error: age is invalid"); } else { console.log("an unexpected error occurred"); } }
In this example, we created a simple schema to verify the input is a number and used min
and max
methods to ensure it’s between 18
and 30
. VineJS offers these additional validation rules to make the validation more precise.
Sometimes, you need to format input data before applying validation rules. For example, if you want to ensure an input string is transformed to lowercase before validation, you can do this within the schema:
const usernameSchema = vine .string() .toLowerCase() .minLength(3) .maxLength(15) .regex(/^[a-z0-9_]+$/); console.log(vine.validate({schema: nameSchema, data: "Bruce_Wayne"})) // logs bruce wayne
In this schema, the username is converted to lowercase before checking its length and format.
Beyond basic schema types, VineJS offers validation for objects and arrays, making it especially useful for validating forms or API payloads with multiple fields.
Let’s look at how you might validate an object representing a user’s profile:
const userProfileSchema = vine.object({ name: vine.string().minLength(3), email: vine.string().email(), age: vine.number().min(18).max(65).optional(), }); const output = await vine.validate({ schema: ageSchema, data: { name: "Jane Doe", email: "[email protected]", age: 29, }, }); // logs { name: 'Jane Doe', email: '[email protected]', age: 29 }
In this example, we’ve set up a schema for a user profile with fields for name
, email
, and age
.
By using vine.object()
method, we can validate each field according to the given rules. All fields in vine.object
are required by default, so they must be present in the object being validated. However, we’ve marked the age
field as optional with the optional()
method, so the validation won’t fail if it’s missing.
Arrays can also be handled similarly:
const tagsSchema = vine .array(vine.string().minLength(2).maxLength(20)) .minLength(1) .maxLength(10); console.log( await vine.validate({ schema: tagsSchema, data: ["tech", "news", "coding"], }) ); // logs [ 'tech', 'news', 'coding' ]
In this example, the schema ensures each item in the array is a string between 2
and 20
characters long, and the array itself must contain 1
to 10
elements. This is especially useful for validating lists like tags or categories.
Pre-compiling is a key feature of VineJS that turns a schema into an optimized JavaScript function that can be reused for validation to help cut down on the overhead of repeatedly parsing and validating the schema. This can be very useful in production environments to provide performance gains.
To pre-compile a schema, you can use the vine.compile()
method:
const compiledSchema = vine.compile( vine.object({ username: vine.string().minLength(3).maxLength(30), password: vine.string().minLength(8), }) ); // Use the compiled schema to validate data console.log( await compiledSchema.validate({ username: "janedoe", password: "password123", }) );
Pre-compiling is particularly useful for schemas that need frequent validation, like those in a high-traffic API endpoint.
Since the schema will be compiled into a reusable function, the repetitive process of parsing and validating the schema is out of the way so that VineJS can speed up the validation process to make your application more responsive.
Custom error messages help provide clearer feedback to users to make it easier to identify and correct mistakes. VineJS uses its built-in SimpleMessagesProvider
API to define error messages as key-value pairs. The key can be a rule name i.e. required
and string
or a specific field-rule combination, and the value is the corresponding error message.
The SimpleMessagesProvider
API can be configured globally, on a per-schema level, or when the validate
method is called. For the code examples that will follow, we’ll use the API globally.
For example, let’s say you want to customize the error messages for a username and email field:
import vine, { SimpleMessagesProvider } from '@vinejs/vine'; vine.messagesProvider = new SimpleMessagesProvider({ 'required': 'You must provide a value for {{ field }}.', 'email': '{{ field }} needs to be a valid email address.', 'username.required': 'A username is required to continue.', });
You can also customize messages for nested fields or array elements. For nested fields, use dot notation:
const messages = { "profile.phone_number.required": "Please provide a phone number.", }; vine.messagesProvider = new SimpleMessagesProvider(messages); const profileSchema = vine.object({ profile: vine.object({ phone_number: vine.string(), }), }); console.log( await vine.validate({ schema: profileSchema, data: { profile: {}, }, }) // throws error: Please provide a phone number. );
For array elements, you can use a wildcard (*
) to target all items or specify an index:
const messages = { "tags.*.required": "Each tag is required.", "tags.0.required": "The first tag is mandatory.", "tags.array.minLength": "There must be at least one tag.", "tags.array.maxLength": "There can't be more than 10 tags.", }; vine.messagesProvider = new SimpleMessagesProvider(messages); const schema = vine.object({ tags: vine .array(vine.string().minLength(2).maxLength(20)) .minLength(1) .maxLength(10), }); console.log( await vine.validate({ schema: schema, data: { tags: [], }, }) // throws error: There must be at least one tag. );
VineJS also allows you to substitute field names with more user-friendly labels. This is helpful when the field names in your code are not suitable for user-facing messages:
const messages = { "first_name.required": "Your {{ field }} is required.", "last_name.required": "Your {{ field }} is required.", }; const fields = { first_name: "first name", last_name: "last name", }; vine.messagesProvider = new SimpleMessagesProvider(messages, fields); const userInfoSchema = vine.object({ first_name: vine.string(), last_name: vine.string(), }); console.log( await vine.validate({ schema: userInfoSchema, data: { first_name: "Bruce", }, }) // throws error: Your last name is required. );
Beyond what the built-in rules provide, VineJS gives developers the ability to create custom validation rules to meet your specific needs. You can use these custom rules in your project by implementing them as stand-alone functions or by integrating them into pre-existing schema classes.
In VineJS, a custom rule is simply a function that updates or validates a field’s value. Three parameters are normally passed to the function: the value to be validated, any options that the rule may need, and the field context.
For example, let’s create a custom rule called mongodbId
that checks if a string is a valid MongoDB ObjectId:
import { ObjectId } from "mongodb"; function mongodbId(value, _options, field) { if (typeof value !== "string" || !ObjectId.isValid(value)) { field.report( "The {{ field }} must be a valid MongoDB ObjectId.", "mongodbId", field ); } }
To make this rule usable within VineJS schemas, we must first convert it into a
VineJS-compatible rule using the vine.createRule
method:
const mongodbIdRule = vine.createRule(mongodbId); const schema = vine.object({ userId: vine.string().use(mongodbIdRule()), }); // throws error: The userId must be a valid MongoDB ObjectId. console.log(vine.validate({ schema, data: { userId: "654628c909ee" } }));
To further simplify its usage, you might want to add the mongodbId
method directly to the VineString
class to benefit from a chainable API:
Zod is a TypeScript-first schema validation library that’s both simple and powerful. It makes defining and enforcing data structures and validation rules easy, and it works well for both frontend and backend applications.
Designed specifically for TypeScript, Zod ensures smooth integration and strong type inference for TypeScript projects.
Some of the key features of Zod are:
Zod makes schema validation straightforward and flexible, allowing you to handle various data types and validation needs with ease. Its syntax is very similar to VineJS as you’ll see in the sections that follow.
Zod handles basic data types like strings, numbers, booleans, and dates well.
For example, let’s create a simple schema for validating a string and number:
import { z } from "zod"; const nameSchema = z.string(); const ageSchema = z.number().min(18); const nameResult = nameSchema.parse("Peter Parker"); console.log(nameResult); // logs Peter Parker const ageResult = ageSchema.parse(16); // throws error: Number must be greater than or equal to 18 console.log(ageResult.success);
In this example, nameSchema
validates that “Peter Parker” is a string and passes, while ageResult
fails because the age is under 18.
When dealing with objects and arrays, Zod makes it straightforward to define the shape of your data. For instance, validating a user object and a list of tags can be done like this:
const userSchema = z.object({ name: z.string(), email: z.string().email(), age: z.number().optional(), }); const tagsSchema = z.array(z.string()); const userData = { name: "Alice", email: "[email protected]", age: 25, }; const tagsData = ["typescript", "zod", 123]; // Invalid, as 123 is not a string const userResult = userSchema.parse(userData); console.log(userResult); const tagsResult = tagsSchema.parse(tagsData); // throws error: Expected string, received number console.log(tagsResult);
In the above example, userSchema
validates the user data and tagsSchema
checks that the array only contains strings. The array validation fails because 123
is not a string.
To make validation feedback more useful and recognizing errors simpler, Zod also supports configurable error messages.
If the age is under 18
, for instance, you can set a personalized message:
import { z } from "zod"; const ageSchema = z.number().min(18, { message: "You must be 18 or older." }); const ageResult = ageSchema.parse(16); // throws error: "You must be 18 or older." console.log(ageResult);
Here, the validation fails, and an error is thrown with the custom error message You must be 18 or older.
Zod provides flexibility for creating custom validation logic using the refine
method, which lets you enforce rules beyond basic type checking.
To validate a hex color code, for example, it is not enough to simply determine whether it is a string; it also needs to adhere to a certain pattern. Here’s how to go about doing it:
const hexColorSchema = z .string() .refine((value) => /^#([0-9A-F]{3}|[0-9A-F]{6})$/i.test(value), { message: "Invalid hex color code. It should be in the format #RRGGBB or #RGB.", }); hexColorSchema.parse("#123AB"); // throws error: "Invalid hex color code. It should be in the format #RRGGBB or #RGB."
In this example, custom validation logic is added using the refine
method to determine whether the string is a valid hex color code consisting of three or six characters (#RGB
or #RRGGBB
).
Benchmarks from the VineJS docs show that VineJS is one of the fastest validation libraries in the Node.js ecosystem, surpassing Yup and Zod in simple object validation and other validation.
The chart shows that VineJS delivers superior performance, making it a good solution for backend applications that need high performance. Zod works well and is fast enough for the majority of use cases.
TypeScript support is excellent in both, but Zod was designed with TypeScript in mind to make type inference more seamless. VineJS also supports TypeScript, but isn’t as deeply integrated, leaving Zod a slight edge for TypeScript-heavy projects.
With more resources, tutorials, and plugins available, Zod has a larger and more established community. However, even though VineJS is newer, has fewer resources, and has a smaller community, it’s expected to grow further because of its easy-to-use API and performance-focused design.
The main drawback of using VineJS is that it isn’t designed to be used in frontend runtimes. It is less suitable for applications that require client-side validation because of this constraint. Additionally, it does not support CommonJS, which could be an issue for projects that use it. It only functions with ECMAScript Modules (ESM).
However, Zod is more versatile, supporting the two major JavaScript module systems while working well regardless of the environment where you’re running your code, which makes it a better fit for full-stack projects.
Apart from VineJS and Zod, a few other libraries for schema validation are worth mentioning for various use cases.
Because of its ease of usage, Yup is well-liked and frequently used in front-end validation, particularly when combined with React and tools like Formik. Compared to VineJS or Zod, it might not function as well with complex structures, but its chainable API makes developing schemas simple.
A powerful library often used in Node.js backends is called joi. Although its API can feel heavier than VineJS and Zod’s lightweight approach, it gives more flexibility and manages complicated validation for deeply nested objects. For server-side apps that require sophisticated validation, it’s perfect.
Speed and complete JSON schema compliance are the main priorities of AJV. Though it lacks the user-friendly APIs that Zod or VineJS has, it’s great for validating JSON data, especially in APIs. But for tasks that require high efficiency, like validating huge JSON datasets, it’s ideal.
VineJS and Zod are two excellent schema validation tools and you won’t go wrong with either of them, but they excel in different areas. If you’re still unsure of which to use, try them both in a small project and see which one feels right for you. Happy coding!
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowMaking carousels can be time-consuming, but it doesn’t have to be. Learn how to use React Snap Carousel to simplify the process.
Consider using a React form library to mitigate the challenges of building and managing forms and surveys.
In this article, you’ll learn how to set up Hoppscotch and which APIs to test it with. Then we’ll discuss alternatives: OpenAPI DevTools and Postman.
Learn to migrate from react-native-camera to VisionCamera, manage permissions, optimize performance, and implement advanced features.
2 Replies to "VineJS vs. Zod for schema validation"
The link to vine is wrong..
Thanks for the note — we’ve corrected it!