Back to Scaling Node.js Applications guides

Using VineJS for Data Validation in NodeJS

Stanley Ulili
Updated on May 7, 2025

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
Output
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"}'
Output
{"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:

Screenshot of Postman 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"}'
Output
{"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:

index.js
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:

  1. Schema definition using the fluent API with chainable methods
  2. Asynchronous validation with automatic error handling
  3. Type safety for the validated output (in TypeScript projects)
  4. 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:

Output
{"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:

Screenshot showing postman error response

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:

index.js
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 the password 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:

Output
{"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:

Screenshot showing validation errors in Postman

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:

index.js
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:

Output
{"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:

Output
...
{"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:

Screenshot of the error message in postman

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.

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Next article
Getting Started with Rspack
Set up and customize Rspack, a fast Rust-based JavaScript bundler with instant startup, HMR, and webpack compatibility.
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

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
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
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.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github