VineJS is a fast, lightweight validation library built for Node.js backends. The team behind AdonisJS created it, but it works with any framework.
Use VineJS to validate form data or JSON in HTTP requests before your app processes it. It focuses only on this task, so it’s faster than general-purpose libraries like Zod or Yup.
VineJS is ESM-only and supports TypeScript. It gives you both runtime checks and type safety. It also takes care of common issues with form data, like how checkboxes or empty fields are handled in HTML.
This guide shows you how to set up VineJS, use it with Express, create validation rules, and deal with validation errors. .
Prerequisites
To use this guide, make sure you're running Node.js version 16.0.0 or higher. VineJS uses modern JavaScript features, so older versions won't work.
VineJS is ESM-only, which means it doesn't support CommonJS. This affects how you set up your project.
Setting up the project directory
Start with a fresh Node.js project set up for ESM modules. VineJS doesn't support CommonJS, so using ESM is required. Set up your project folder and initialize it:
mkdir vinejs-demo && cd vinejs-demo
npm init -y
The key step is enabling ECMAScript Modules. Since VineJS doesn’t work with require()
, you need to set "type": "module"
in your package.json
:
npm pkg set type=module
Next, install the necessary packages. You'll use Express to build the API and VineJS to handle validation:
npm install express @vinejs/vine
This setup makes your project compatible with VineJS’s ESM-only structure, so you can use modern import
and export
syntax across your code.
Creating a basic Express app without validation
Before using VineJS, let’s look at a simple Express app without any validation. This will help you understand the risks of skipping data checks. Many developers focus on making things work first, which can leave APIs open to bad or unexpected data.
Create a file called index.js
and add this basic Express setup:
import express from "express";
const app = express();
const PORT = 3000;
// Middleware to parse JSON request bodies
app.use(express.json());
app.get("/", (req, res) => {
res.send("Welcome to the VineJS Validation Demo!");
});
// User registration endpoint without validation
app.post("/register", (req, res) => {
const { name, email, password } = req.body;
// Store user data (in a real app, this would go to a database)
const user = { name, email, password };
res.status(201).json({
message: "User registered successfully",
user: { name, email }
});
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
This app sets up a /register
endpoint that accepts incoming data without checking it. It simply pulls values from the request body, but doesn’t validate them. This is a common early-stage pattern, but it opens the door to serious data integrity problems.
To run the app and have it auto-restart on changes, use Node’s watch mode:
node --watch index.js
Server running on http://localhost:3000
Completed running 'index.js'
Let's test the endpoint with valid data first:
curl -X POST http://localhost:3000/register \
-H "Content-Type: application/json" \
-d '{"name":"John Doe", "email":"john@example.com", "password":"password123"}'
{"message":"User registered successfully","user":{"name":"John Doe","email":"john@example.com"}}
If you're using Postman, create a new POST
request to http://localhost:3000/register
, set the Content-Type
to application/json
, and add the following JSON in the body:
{
"name": "John Doe",
"email": "john@example.com",
"password": "password123"
}
You will get similar output:
Now, let's try completely invalid data to see how our application responds:
curl -X POST http://localhost:3000/register \
-H "Content-Type: application/json" \
-d '{"name":"", "email":"not-an-email", "password":"123"}'
{"message":"User registered successfully","user":{"name":"","email":"not-an-email"}}
The application accepts clearly problematic input without any objection. This naive implementation introduces several risks:
- Empty names that could break UI displays or database constraints
- Invalid email addresses that will fail delivery or verification processes
- Dangerously short passwords that compromise account security
- Potential for SQL injection if this data is used in raw queries
- No protection against malicious payloads or script injection
- Missing required fields that could cause application errors downstream
These issues highlight the need for a good validation solution like VineJS, which is specifically optimized for handling form data in Node.js applications.
Getting started with VineJS
VineJS takes a focused approach to validation by handling only HTTP request bodies, with first-class TypeScript support built in.
To start using VineJS, update your index.js
file like this:
import express from "express";
import vine from "@vinejs/vine";
const app = express();
const PORT = 3000;
// Define validation schema using VineJS's object schema builder
const schema = vine.object({
name: vine.string().trim().minLength(2).maxLength(50),
email: vine.string().email(),
password: vine.string().minLength(8)
});
// Middleware to parse JSON request bodies
app.use(express.json());
app.get("/", (req, res) => {
res.send("Welcome to the VineJS Validation Demo!");
});
// User registration endpoint with validation
app.post("/register", async (req, res) => {
try {
// VineJS returns a Promise, so we use await here
const validatedData = await vine.validate({ schema, data: req.body });
// Process validated data
res.status(201).json({
message: "User registered successfully",
user: {
name: validatedData.name,
email: validatedData.email
}
});
} catch (error) {
// Handle validation errors
return res.status(400).json({ errors: error.messages });
}
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
This implementation uses VineJS's core features:
- Schema definition using the fluent API with chainable methods
- Asynchronous validation with automatic error handling
- Type safety for the validated output (in TypeScript projects)
- Automatic error formatting with detailed field-specific messages
Save the changes and test the endpoint with our previously invalid data:
curl -X POST http://localhost:3000/register \
-H "Content-Type: application/json" \
-d '{"name":"", "email":"not-an-email", "password":"123"}'
This time, VineJS intercepts the invalid input and generates a structured error response:
{"errors":[{"message":"The name field must have at least 2 characters","rule":"minLength","field":"name","meta":{"min":2}},{"message":"The email field must be a valid email address","rule":"email","field":"email"},{"message":"The password field must have at least 8 characters","rule":"minLength","field":"password","meta":{"min":8}}]}
If you're using Postman, the response will look similar to this when you send invalid data:
The response now includes specific validation failures for each field, guiding the client on exactly what needs correction. Unlike some validation libraries that return at the first error encountered, VineJS collects all validation errors in a single pass, providing a comprehensive overview of input issues.
Understanding VineJS validation chains
VineJS builds validation logic through schema chains that define both data structure and validation rules. Unlike annotation-based validators, VineJS uses a fluent API where each method narrows down what data is acceptable.
Basic validation chains
Every validation in VineJS starts with a schema type that sets the foundation for the data's nature. Each schema type then offers specific validators and modifiers:
import express from "express";
import vine from "@vinejs/vine";
import { SimpleMessagesProvider } from "@vinejs/vine";
const app = express();
const PORT = 3000;
// Middleware to parse JSON request bodies
app.use(express.json());
// Define validation schema at the top level of your file
const schema = vine.object({
name: vine
.string()
.trim()
.minLength(2)
.maxLength(50),
email: vine
.string()
.trim()
.email()
.normalizeEmail(),
password: vine
.string()
.minLength(8)
.maxLength(100)
.regex(/[A-Z]/)
.regex(/[0-9]/),
password_confirmation: vine
.string()
});
// Define custom messages
vine.messagesProvider = new SimpleMessagesProvider({
minLength: 'The {{ field }} field must have at least {{ min }} characters',
maxLength: 'The {{ field }} field must not be greater than {{ max }} characters',
email: 'Please provide a valid email address',
regex: 'The {{ field }} field format is invalid'
});
app.get("/", (req, res) => {
res.send("Welcome to the VineJS Validation Demo!");
});
// User registration endpoint with validation
app.post("/register", async (req, res) => {
try {
// Pre-compile the schema for performance optimization
const validator = vine.compile(schema);
const validatedData = await validator.validate(req.body);
// Work with validated data, which now matches the schema exactly
res.status(201).json({
message: "Registration successful",
user: {
name: validatedData.name,
email: validatedData.email
}
});
} catch (error) {
return res.status(400).json({ errors: error.messages });
}
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
VineJS's clear separation between data transformation and validationsets it apart. Modifiers like trim()
change the input, while validators like email()
simply check the value without altering it.
Here are some key features of this setup:
- The
password_confirmation
field automatically matches against thepassword
field. - The schema is compiled into efficient JavaScript, making it fast for repeated validation.
- You can define custom error messages globally using the
messagesProvider
. - VineJS validates all fields in one go, collecting every error instead of stopping at the first failure.
When you test with invalid input, VineJS returns a full list of validation issues in a single response:
curl -X POST http://localhost:3000/register \
-H "Content-Type: application/json" \
-d '{"name":"J", "email":"not-an-email", "password":"password"}'
The response includes errors for all fields with validation failures:
{"errors":[{"message":"The name field must have at least 2 characters","rule":"minLength","field":"name","meta":{"min":2}},{"message":"Please provide a valid email address","rule":"email","field":"email"},{"message":"The password field format is invalid","rule":"regex","field":"password"},{"message":"The password_confirmation field must be defined","rule":"required","field":"password_confirmation"}]}
If you're using Postman, the response will look similar to this when you submit invalid data:
Validating nested objects and arrays
As your app grows, you'll often need to validate nested objects and arrays. VineJS makes this straightforward using its fluent schema syntax.
Here’s how you can validate a user profile with an address object and a list of skills:
import express from "express";
import vine from "@vinejs/vine";
const app = express();
const PORT = 3000;
// Middleware to parse JSON request bodies
app.use(express.json());
// Define a schema with nested object and array validation
const schema = vine.object({
name: vine.string().trim().minLength(2).maxLength(50),
email: vine.string().email().normalizeEmail(),
password: vine
.string()
.minLength(8)
.maxLength(100)
.regex(/[A-Z]/)
.regex(/[0-9]/),
password_confirmation: vine.string(),
address: vine.object({
street: vine.string().minLength(3),
city: vine.string().minLength(2),
zip: vine.string().regex(/^\d{5}$/)
}),
skills: vine.array(vine.string().minLength(2)).minLength(1)
});
...
In this code, you validate nested data structures using VineJS. The address
field is a nested object with its own rules for street
, city
, and zip
, ensuring structured and valid location data.
The skills
field is an array where each item must be a string of at least 2 characters, and the array must contain at least one item. This shows how VineJS cleanly handles nested objects and arrays within a single schema.
Now test with valid data:
curl -X POST http://localhost:3000/register \
-H "Content-Type: application/json" \
-d '{
"name": "Jane",
"email": "jane@example.com",
"password": "Password123",
"password_confirmation": "Password123",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zip": "12345"
},
"skills": ["JavaScript", "Node.js"]
}'
You will see output that looks like this:
{"message":"User registered successfully","user":{"name":"Jane","email":"jane@example.com"}}
Now test with invalid data:
curl -X POST http://localhost:3000/register \
-H "Content-Type: application/json" \
-d '{
"name": "J",
"email": "invalid-email",
"password": "pass",
"password_confirmation": "",
"address": {
"street": "",
"city": "",
"zip": "abc"
},
"skills": []
}'
You will receive output similar to this:
...
{"message":"The street field must have at least 3 characters","rule":"minLength","field":"address.street","meta":{"min":3}},{"message":"The city field must have at least 2 characters","rule":"minLength","field":"address.city","meta":{"min":2}},{"message":"The zip field format is invalid","rule":"regex","field":"address.zip"},{"message":"The skills field must have at least 1 items","rule":"array.minLength","field":"skills","meta":{"min":1}}]}
If you're using Postman, this structured error response will appear clearly in the response body, showing all nested field errors in detail:
That takes care of using VineJS to validate nested objects and arrays.
Final thoughts
VineJS makes it easy to validate data in Node.js apps. You can define clear rules for fields, nested objects, arrays, and even conditional logic.
It runs fast, supports TypeScript, and keeps your code clean and easy to maintain. Whether you're building simple forms or complex APIs, VineJS helps you catch bad data early.
To learn more, visit the official VineJS documentation.
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github