10 Best Node.js Data Validation Libraries
Compare the 10 best Node.js validation libraries by type inference, JSON Schema support, integrations, errors, performance, and operational fit.

TypeScript types disappear when your program runs. Request bodies, environment variables, webhook payloads, queue messages, and database results can still contain missing, malformed, or hostile values. A runtime validation library checks those values at the boundary of your application and gives you a controlled result.
Short answer: choose Zod for a TypeScript-first API, Joi for mature server-side rules, and Ajv when JSON Schema, OpenAPI, or compiled validators matter. Use Yup for browser and form workflows, class-validator for decorator-based DTOs, and the remaining libraries when their programming model or footprint matches your system. There is no universal performance winner without a controlled benchmark using the same versions, schemas, and workloads.
How to choose a Node.js validation library
Evaluate each candidate against the boundary you need to protect and the shape your team already uses.

| Decision axis | Questions to ask |
|---|---|
| Type inference | Can a schema produce a useful TypeScript type, or will you maintain duplicate declarations? |
| Schema interoperability | Must the contract be portable as JSON Schema or JSON Type Definition across services and languages? |
| Validation style | Will developers prefer fluent schemas, functional codecs, decorators, or Express middleware? |
| Transformation | Do you need trimming, casting, defaults, coercion, or output shaping? |
| Error model | Do you need path-aware issues, all errors at once, or abort-early behavior? |
| Async rules | Will validation call a database, service, or custom asynchronous format? |
| Integration | Which adapters exist for Express, Fastify, NestJS, React forms, OpenAPI, and generated clients? |
| Operations | How do startup cost, throughput, bundle size, maintenance, and ecosystem maturity affect deployment? |
1. Zod: best default for TypeScript-first APIs
Zod lets you define a schema once, validate unknown input, and infer a static TypeScript type from that schema. Its API is procedural and readable, which makes it a strong default for Express, Fastify, serverless handlers, and shared domain packages. The Zod documentation compares its design with Joi, Yup, and io-ts; it also notes that io-ts heavily influenced Zod’s API (Zod documentation).
import express from 'express';
import { z } from 'zod';
const app = express();
app.use(express.json());
const CreateUser = z.object({
email: z.string().email(),
name: z.string().trim().min(1).max(80),
age: z.number().int().min(13).optional()
});
type CreateUserInput = z.infer<typeof CreateUser>;
app.post('/users', (req, res) => {
const result = CreateUser.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'invalid_request',
issues: result.error.issues
});
}
const user: CreateUserInput = result.data;
return res.status(201).json({ user });
});
app.listen(3000);
Use safeParse when invalid input is expected and should become a normal HTTP response. Use parse when an exception is appropriate. Decide explicitly whether unknown object keys should be stripped, passed through, or rejected, and whether coercion belongs at the boundary.
2. Joi: mature server-side validation and business rules
Joi is a mature choice for JavaScript services with complex, conditional rules and a broad validation API. Its fluent schemas are useful when requirements are expressed as business constraints rather than shared TypeScript types. Joi’s official documentation covers the extensive rule and customization surface (Joi API).

import Joi from 'joi';
const orderSchema = Joi.object({
currency: Joi.string().valid('USD', 'EUR').required(),
total: Joi.number().positive().required(),
coupon: Joi.string().max(40).when('total', {
is: Joi.number().greater(100),
then: Joi.required(),
otherwise: Joi.forbidden()
})
});
const { error, value } = orderSchema.validate(input, {
abortEarly: false,
convert: true,
allowUnknown: false
});
Review conversion and unknown-key settings carefully. A schema that silently converts values or accepts extra keys can produce surprising domain objects.
3. Ajv: best for JSON Schema and compiled validation
Ajv is the standards-first option when contracts must be JSON Schema or JSON Type Definition. It supports JSON Schema drafts through 2020-12 and compiles schemas into validation functions. Ajv’s documentation describes generated code designed for V8 optimization (Ajv documentation).
import Ajv from 'ajv';
const ajv = new Ajv({ allErrors: true, strict: true });
const schema = {
type: 'object',
properties: {
email: { type: 'string', format: 'email' },
enabled: { type: 'boolean' }
},
required: ['email'],
additionalProperties: false
};
const validate = ajv.compile(schema);
if (!validate(input)) {
console.error(validate.errors);
} else {
console.log('valid', input);
}
Ajv fits OpenAPI-oriented contracts, generated clients, and systems where the same schema is consumed by multiple languages. Treat schemas as versioned API artifacts. Configure formats, strictness, and unknown keywords deliberately.
4. Yup: browser and form-heavy workflows
Yup is especially useful in frontend forms. Casting, transforms, defaults, and field-oriented error messages make it convenient when user input moves through a form before submission. It can also validate server payloads, but decide whether its casting behavior is acceptable for a security boundary. See the Yup project documentation.
import * as yup from 'yup';
const profileSchema = yup.object({
displayName: yup.string().trim().min(2).required(),
newsletter: yup.boolean().default(false)
});
const profile = await profileSchema.validate(input, {
abortEarly: false,
stripUnknown: true
});
5. class-validator: decorator-based TypeScript DTOs
class-validator suits teams already using decorator-based DTOs, particularly in NestJS-style applications. Validation rules live beside class properties, which can be convenient for transport objects. It introduces decorator and reflection conventions, so assess whether those conventions fit libraries shared outside the framework. The project documents its decorators and validation container (class-validator).
import { IsEmail, IsInt, IsOptional, Min, validate } from 'class-validator';
class CreateUserDto {
@IsEmail()
email!: string;
@IsOptional()
@IsInt()
@Min(13)
age?: number;
}
const dto = Object.assign(new CreateUserDto(), input);
const errors = await validate(dto);
6. io-ts: functional runtime codecs
io-ts is a good fit for teams comfortable with functional programming and explicit runtime type codecs. Decoding returns an explicit success or failure value, encouraging callers to handle invalid data as data. Its abstractions can feel heavier than a fluent schema for teams unfamiliar with functional patterns. The io-ts documentation explains its codec model.
7. Valibot: modular, lightweight schemas
Valibot is a lightweight alternative worth evaluating when bundle size and modularity matter. Verify current feature coverage, integrations, and release compatibility before standardizing because the library’s capabilities evolve. Start with a representative schema from your application rather than a synthetic example. See Valibot documentation.
8. Superstruct: compact composable validation
Superstruct provides a compact, composable API for JavaScript and TypeScript. It can be a practical choice for small services or modules that want explicit validators without a large framework commitment. Check how its error objects and coercion behavior map to your API conventions. See Superstruct.
9. express-validator: middleware and sanitization in Express
express-validator is designed for Express middleware chains. It is a natural fit when validation and sanitization should be attached directly to routes and composed with existing middleware. The tradeoff is that rules are distributed across chains instead of represented as one portable object schema. See express-validator documentation.
import { body, validationResult } from 'express-validator';
app.post('/login',
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 12 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
res.sendStatus(204);
}
);
10. validator.js: string validation and sanitization utilities
validator.js focuses on string predicates and sanitizers such as email, URL, length, and normalization checks. It is often combined with a higher-level object schema library rather than used alone for nested request contracts. See the validator.js repository.
Zod vs Joi vs Yup: a practical decision
| Need | Start with | Reason |
|---|---|---|
| One schema and inferred TypeScript type | Zod | TypeScript-first API with direct inference |
| Complex server-side conditions | Joi | Mature fluent rule set and customization |
| Forms, casting, and transforms | Yup | Convenient browser-oriented behavior |
| Portable contract | Ajv | JSON Schema and generated validators |
| Decorated DTO classes | class-validator | Rules attached to TypeScript classes |
Validate an Express request body safely
- Parse JSON with a body-size limit appropriate to the endpoint.
- Validate immediately at the route boundary, before business logic or database calls.
- Return a stable error shape with field paths; do not expose stack traces or secrets.
- Use the validated result, not the original request object, downstream.
- Add tests for missing fields, wrong types, extra keys, boundary values, and malformed JSON.
import express from 'express';
import { z } from 'zod';
const app = express();
app.use(express.json({ limit: '100kb' }));
const Search = z.object({
q: z.string().trim().min(1).max(200),
page: z.coerce.number().int().min(1).default(1)
}).strict();
app.post('/search', (req, res) => {
const parsed = Search.safeParse(req.body);
if (!parsed.success) {
return res.status(422).json({
error: 'validation_failed',
fields: parsed.error.flatten().fieldErrors
});
}
const { q, page } = parsed.data;
return res.json({ q, page });
});
app.listen(3000, () => console.log('listening on 3000'));
Run it with npm i express zod and a TypeScript runner such as your existing project setup. Test the boundary with cURL:
curl -i http://localhost:3000/search \
-H 'content-type: application/json' \
--data '{"q":"node validation","page":"2"}'
A Python client can exercise the same endpoint:
import requests
response = requests.post(
'http://localhost:3000/search',
json={'q': 'node validation', 'page': '2'},
timeout=10,
)
response.raise_for_status()
print(response.json())
Performance, reliability, and cost considerations
Do not rank these libraries by an isolated benchmark. Versions, schema complexity, warm-up, error collection, coercion, and input distribution all change results. Ajv compiles validators, which can reduce repeated validation work after startup; measure compilation and steady-state costs separately. For every library, profile realistic payloads and include invalid traffic.
Keep schemas reusable so they are not rebuilt per request. Compile or initialize validators during startup when the library supports it. Set request-size limits, avoid expensive asynchronous checks for obviously malformed data, and cache stable reference data used by custom rules. Validation is usually cheaper than downstream database work, but unbounded payloads and pathological regular expressions can still consume CPU.
Libraries are generally open-source dependencies, so direct per-request licensing cost is not the deciding factor. Your operational cost comes from CPU, memory, cold starts, bundle size, maintenance, and the engineering time required to map errors into a stable API. Pin versions, review changelogs, and keep a small compatibility test suite for schemas shared across services.
Troubleshooting common validation failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Every field is undefined | JSON middleware is missing or mounted after the route | Mount express.json() before routes and verify the content type. |
| Numbers fail when sent as strings | Strict type validation rejects JSON strings | Use explicit coercion only where it is safe, then test values such as empty strings and decimals. |
| Valid fields disappear | Unknown-key stripping or schema transform is enabled | Choose pass-through, strip, or reject behavior deliberately and document it. |
| Only the first error appears | Abort-early mode is enabled | Enable all-errors mode when clients need field-level feedback; keep early abort for hot paths if appropriate. |
| Errors expose internals | Raw exception objects are returned | Map library errors to a stable public shape and log the detailed object privately. |
| Async custom rule never runs | A synchronous parse method was used | Use the library’s async API and await it; keep network checks out of cheap structural validation. |
| Ajv rejects a schema at startup | Strict mode, unsupported keywords, or draft mismatch | Match the configured draft, register formats or keywords explicitly, and fix schema errors before deployment. |
Or skip the browser setup: ScreenshotNeo for documentation captures
If your validation project needs screenshots of API documentation, examples, or rendered test pages, ScreenshotNeo is the alternative to try first. It returns a clean PNG, JPEG, WebP, or PDF from one request. Cookie and consent banners, newsletter popups, and chat widgets are removed before capture; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
See the ScreenshotNeo API docs for all options, including full-page capture, CSS selectors, custom CSS and JavaScript, waiting rules, blocking, headers, cookies, viewport and device settings, PDFs, caching, signed links, async jobs, bulk capture, and usage reporting.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
The free plan includes 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.
FAQ
Should I validate TypeScript values at runtime?
Yes, whenever data crosses a trust boundary: HTTP requests, webhooks, queues, configuration, files, or databases.
Can one project use more than one library?
Yes. For example, Ajv can own a shared JSON Schema contract while Zod handles local domain parsing. Keep ownership clear and avoid silently changing semantics between layers.
How should validation errors be localized?
Return stable field paths and machine-readable codes from the API. Translate messages at the client or presentation layer when needed.
Which library is fastest?
No universal answer is supported by the available evidence. Benchmark your versions, schemas, valid and invalid payloads, startup path, and production runtime.
Do sanitizers replace validation?
No. Normalization can make input consistent, but you still need explicit type, range, shape, and authorization checks.
