Input Validation and Sanitization in Node.js

· 5 min read · Updated March 19, 2026 · intermediate
javascript node security validation express xss sql-injection

Why Input Validation Matters

Every piece of data that enters your application from the outside is potentially dangerous. Users can accidentally submit bad data (typos, wrong formats) or deliberately try to break things (hackers submitting malicious code). Without validation, your app might crash, store corrupted data, or worse — become vulnerable to attacks.

Think of input validation like a bouncer at a club. They check your ID, make sure you’re on the list, and turn away anyone who doesn’t meet the rules. Your app needs the same bouncer for every piece of user input.

There are two related concepts here:

  • Validation checks if input meets your rules (is this a valid email?)
  • Sanitization cleans input to make it safe (remove dangerous characters)

You need both. Validation keeps bad data out. Sanitization makes sure even “valid” data won’t cause harm when you use it.

Schema Validation with Zod

Zod is a library that lets you define the exact shape of data your app accepts. It works great with TypeScript but works fine in plain JavaScript too.

First, install it:

npm install zod

Here’s how you define a simple user schema:

import * as z from "zod";

// Define what valid user data looks like
const UserSchema = z.object({
  username: z.string().min(3).max(20),
  email: z.string().email(),
  age: z.number().min(0).optional(),
});

// Try to validate some input
const result = UserSchema.safeParse(someInput);

// Check if it passed
if (result.success) {
  console.log(result.data); // Valid data with correct types
} else {
  console.log(result.error.issues); // Array of validation errors
}

The key methods are parse() (throws on error) and safeParse() (returns an object you can check). Always use safeParse in production — it won’t crash your server with uncaught exceptions.

Zod can also infer TypeScript types from your schema automatically:

type User = z.infer<typeof UserSchema>; // { username: string; email: string; age?: number }

Joi for JavaScript Projects

Joi is another popular choice, especially if you’re not using TypeScript. It has a similar purpose but a different style.

npm install joi
import Joi from "joi";

const schema = Joi.object({
  name: Joi.string().min(1).max(100).required(),
  age: Joi.number().integer().min(0).optional(),
  email: Joi.string().email(),
});

// Validate
const { error, value } = schema.validate(userInput);

if (error) {
  console.log(error.details); // List of validation errors
}

One thing to watch: Joi tries to convert input types by default. A string "25" might become a number 25. Pass { convert: false } if you need strict type checking.

Yup for Form-Like Validation

Yup looks a lot like Joi but integrates nicely with form libraries. It’s a good choice if you’re building forms.

npm install yup
import { object, string, number } from "yup";

const schema = object({
  username: string().required().min(3),
  email: string().email().required(),
  age: number().min(0).integer().optional(),
});

// Sync or async validation
schema.validateSync(data); // throws on error
await schema.validate(data); // returns Promise, rejects on error

Yup also has transforms, which let you clean or modify data during validation:

const schema = string()
  .trim()      // remove whitespace from ends
  .lowercase() // convert to lowercase
  .email();

express-validator for Express Routes

If you’re using Express, this library integrates directly with your route handlers. It lets you define validation rules right in your route.

npm install express-validator
import express from "express";
import { body, validationResult } from "express-validator";

const app = express();
app.use(express.json());

app.post("/users", [
  body("username").isLength({ min: 3 }).isAlphanumeric(),
  body("email").isEmail().normalizeEmail(),
  body("age").optional().isInt({ min: 0 }),
], (req, res) => {
  const errors = validationResult(req);
  
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  
  // req.body is now validated and sanitized
  const { username, email, age } = req.body;
  
  res.json({ message: "User created successfully" });
});

The validators run in order, and sanitizers modify values before the next validator sees them. normalizeEmail() cleans up the email address, and isInt() ensures age is a valid integer.

Preventing XSS Attacks

XSS (cross-site scripting) happens when untrusted data gets executed as JavaScript in a browser. This occurs when you take user input and put it directly into HTML without encoding.

The simplest rule: never use .innerHTML with raw user input.

// UNSAFE
element.innerHTML = userInput;

// SAFE - use textContent instead
element.textContent = userInput;

If you need to render HTML from user input, you must sanitize it first:

npm install dompurify
import DOMPurify from "dompurify";

const clean = DOMPurify.sanitize(dirtyHtml, {
  ALLOWED_TAGS: ["b", "i", "em", "strong", "a"],
  ALLOWED_ATTR: ["href", "target", "rel"],
});

DOMPurify strips out dangerous tags and attributes while keeping safe formatting tags.

SQL Injection Prevention

SQL injection happens when attackers insert malicious SQL commands into your queries through user input. The fix is simple: never concatenate user input into SQL strings.

// UNSAFE - don't do this
const query = "SELECT * FROM users WHERE email = '" + userEmail + "'";
db.query(query);

// SAFE - use parameterized queries
const result = await db.query(
  "SELECT * FROM users WHERE email = $1",
  [userEmail]
);

Different database drivers use different placeholders:

  • pg (PostgreSQL): $1, $2
  • mysql2: ?
  • better-sqlite3: ?

Best Practices Summary

Here’s what you should do:

  1. Validate early — check input at the entry point of your application
  2. Use schema validation libraries — Zod, Joi, Yup, or express-validator handle the heavy lifting
  3. Sanitize for the output context — HTML encoding for web pages, parameterized queries for databases
  4. Keep dependencies updated — security libraries like DOMPurify get patches for new bypass techniques
  5. Validate on the server — client-side validation is convenient but can be bypassed

Remember: validation and sanitization are two different things. Validation decides what to accept. Sanitization makes sure accepted data is safe to use. You need both.

See Also