validator
validator.js is a flat collection of around a hundred string checks and a couple dozen string sanitizers. Each one is a plain function: isEmail(str) returns a boolean, trim(str) returns a string, toInt(str) returns a number or NaN. It covers the formats people keep reimplementing badly, including emails, URLs, IP addresses, UUIDs, credit cards, IBAN and BIC codes, ISO date and country codes, postal codes and mobile phone numbers by locale, hashes, JWTs, and base64. The sanitizer half handles trimming, whitelisting or blacklisting characters, stripping control characters, HTML entity escaping, and canonicalizing an email address so that two spellings of the same Gmail inbox compare equal. It is deliberately not a schema library: there is no object shape, no chaining, and no type inference, just functions you call one at a time.
A dependable grab bag of format checks that has been maintained for over a decade, and the right answer when you need IBAN or credit card validation without writing the regex. Just do not use it as your validation layer; pair it with a schema library and import one function at a time.
Use it if
- You need a specific format check that is genuinely hard to get right by hand, like IBAN, BIC, credit card, ISO 6346 container ID, or a per-locale postal code
- You already have a schema library and want to plug format checks into it, which is exactly what a zod .refine or a class-validator decorator wants
- You want zero runtime dependencies and a per-function import path, so a single check pulls in a few hundred bytes instead of the whole library
- You need email canonicalization: normalizeEmail knows the Gmail dot and plus rules, the Outlook and Yahoo subaddress rules, and the googlemail.com alias
- You are validating input that is already a string, such as query parameters, form fields, or CSV cells
- You are validating objects rather than strings: validator has no schema, no composition, and no TypeScript inference, so zod or valibot does the actual job you have and can call into validator for the odd format
- You import the whole package into a browser bundle: the full build is about 39 KB gzipped, which is a lot next to valibot for the common case of checking three fields
- Your input might not be a string: the README says so in bold, and passing a number, null, or undefined throws rather than returning false, so every call site needs a typeof guard
- You want TypeScript types in the box: there are none, and the community @types/validator package trails the runtime, sitting at 13.15.10 while the library is on 13.15.35
- You think escape() protects you from XSS: it only swaps a fixed set of characters for HTML entities, the library removed real XSS sanitization years ago, and its own README points you at DOMPurify instead
- You need serious phone number handling: isMobilePhone is a per-locale regex list, while libphonenumber-js parses, normalizes, and formats numbers properly
- You accept user-supplied regexes through matches(): the README warns the pattern is not checked for catastrophic backtracking, so that is a denial of service waiting to happen
Setup reality
npm install validator and you are done: no dependencies, no build step, no peer requirements, and an engines field so loose it still claims Node 0.10. The friction shows up in two places. First, TypeScript. Nothing ships in the package, so you add @types/validator as a dev dependency, and because those types are maintained separately they lag the runtime, which means a check added in a recent patch release may have no declaration yet. Second, import shape. The package has no exports map, so what you write depends on your toolchain: require('validator') or a default ESM import gets the whole thing, validator/lib/isEmail gets one CommonJS function, and validator/es/lib/isEmail gets the tree-shakeable ESM build. Mixing those in one codebase is how you end up shipping the entire library alongside the single function you thought you were importing. Beyond that, the one rule that matters is that everything takes a string. Anything else throws, so untrusted input needs a typeof check first or a String(x) coercion, and the README suggests input + '' for exactly that reason.
Patterns
Check an email address with the options that mattervalidate-email
import validator from 'validator'
validator.isEmail('ada@example.com') // true
validator.isEmail('Ada Lovelace <ada@example.com>') // false
validator.isEmail('Ada Lovelace <ada@example.com>', { allow_display_name: true }) // true
validator.isEmail(input, {
domain_specific_validation: true, // apply Gmail's stricter rules
blacklisted_chars: '+', // reject plus addressing
host_blacklist: ['mailinator.com'],
})A true result only means the string is syntactically an address. It says nothing about whether the mailbox exists or the domain has an MX record, so a confirmation email is still the only real check.
Validate a URL without accepting nonsensevalidate-url
validator.isURL('example.com') // true, no protocol needed by default
validator.isURL('example.com', { require_protocol: true }) // false
validator.isURL(input, {
protocols: ['https'],
require_protocol: true,
require_valid_protocol: true,
disallow_auth: true,
host_blacklist: ['localhost', '127.0.0.1'],
})The README marks require_protocol as recommended for a reason: without it, a string carrying authentication info cannot be told apart from a plain host. disallow_auth and a host blacklist are the minimum if the URL will ever be fetched server-side.
Stop a number or undefined from throwingguard-non-strings
function isEmailSafe(value: unknown): boolean {
return typeof value === 'string' && validator.isEmail(value)
}
// req.query values can be string | string[] | undefined
const raw = req.query.email
if (!isEmailSafe(raw)) return res.status(400).json({ error: 'bad email' })validator throws on any non-string input rather than returning false. Express query parameters, JSON bodies, and form data all deliver non-strings often enough that an unguarded call is a 500 waiting to happen.
Import one function instead of the whole librarytree-shakeable-import
// ESM, tree-shakeable build
import isEmail from 'validator/es/lib/isEmail'
import isURL from 'validator/es/lib/isURL'
// CommonJS single function
const isEmail = require('validator/lib/isEmail')
// pulls in everything, about 39 KB gzipped
import validator from 'validator'The package has no exports map, so these three forms all resolve and all behave differently in a bundle. One stray full import anywhere in the dependency graph undoes the per-function imports everywhere else.
Canonicalize an address before storing itnormalize-email
validator.normalizeEmail('Foo.Bar+news@GoogleMail.com')
// 'foobar@gmail.com'
validator.normalizeEmail(input, {
gmail_remove_dots: false, // keep dots if your users expect them preserved
gmail_remove_subaddress: true,
all_lowercase: true,
})This does not validate, so run isEmail first. Storing only the normalized form blocks the plus-addressing trick for duplicate signups, but it also merges accounts some users consider separate, so many apps store both the raw and normalized values.
Trim, strip control characters, and restrict the alphabetsanitize-input
let name = validator.trim(input)
name = validator.stripLow(name) // drop chars < 32 and 127
name = validator.blacklist(name, '<>\\\\') // remove these characters
const slug = validator.whitelist(input.toLowerCase(), 'a-z0-9\\-')
const notes = validator.stripLow(input, true) // keep \n and \rblacklist and whitelist put your characters straight into a RegExp character class, so backslashes and brackets need escaping. stripLow removes newlines unless you pass true as the second argument, which quietly flattens multi-line text.
Escape HTML entities, and know what that does not coverescape-html
validator.escape('<script>alert(1)</script>')
// '<script>alert(1)</script>'
validator.unescape('<b>') // '<b>'escape() replaces < > & ' " ` \ and / with entities. That is safe for text nodes and nothing else: it will not protect an href, a style block, or an inline event handler. For HTML you intend to render, use DOMPurify, which the README recommends after XSS sanitization was removed from this library.
Convert strings to numbers, booleans, and datescoerce-values
const page = validator.toInt(req.query.page ?? '', 10)
if (Number.isNaN(page)) return badRequest('page must be an integer')
validator.toFloat('3.14') // 3.14
validator.toBoolean('yes') // true (anything but '0', 'false', '')
validator.toBoolean('yes', true) // false (strict: only '1' and 'true')
validator.toDate('2026-08-06') // Date, or null if unparseabletoInt and toFloat return NaN on failure rather than throwing, and NaN is falsy, so a plain if check treats a failed parse and a legitimate 0 the same way. toDate returns null instead, so the two failure modes need different handling.
Score or gate a passwordstrong-password
validator.isStrongPassword(pw, {
minLength: 12,
minLowercase: 1,
minUppercase: 1,
minNumbers: 1,
minSymbols: 1,
}) // boolean
const score = validator.isStrongPassword(pw, { returnScore: true })
// number: use it to drive a strength meterreturnScore flips the return type from boolean to number, which TypeScript cannot narrow for you through a variable options object. This is composition rules only, so it happily accepts Passw0rd! and knows nothing about breach lists.
Check UUIDs, JSON, and other identifiersvalidate-ids
validator.isUUID(id) // any RFC 9562 version
validator.isUUID(id, '4') // v4 only
validator.isUUID(id, 'loose') // UUID-shaped hex, ignoring version and variant bits
validator.isJSON(body) // objects and arrays only
validator.isJSON('true', { allow_primitives: true }) // true
validator.isJWT(token)
validator.isBase64(blob, { urlSafe: true })isUUID defaults to strict RFC checking, which rejects the version-less identifiers some older systems emit; that is what 'loose' is for. isJSON runs JSON.parse internally, so validating a large body costs a full parse you are about to repeat.
Validate locale-specific formatslocale-validators
validator.isPostalCode('SW1A 1AA', 'GB')
validator.isMobilePhone('+919876543210', 'en-IN', { strictMode: true })
validator.isMobilePhone(input, ['en-IN', 'en-US'])
validator.isAlpha('Zoë', 'nl-NL')
validator.isAlphaLocales // the supported list, at runtime
validator.isMobilePhoneLocalesPassing no locale means 'any', which matches if a single locale in the whole list matches and is nearly useless as a check. These are regex tables maintained by contributors, so coverage varies by country and lags number plan changes.
Get types, and work around the ones that are missingtypescript-setup
$ npm i -D @types/validator
import isEmail from 'validator/lib/isEmail'
import isIBAN from 'validator/lib/isIBAN'
export function assertEmail(value: unknown): asserts value is string {
if (typeof value !== 'string' || !isEmail(value)) {
throw new TypeError('not an email')
}
}@types/validator is community maintained on DefinitelyTyped and trails the runtime package, so a validator added in a recent patch may not have a declaration. An assertion function like this is also how you get the typeof guard and the format check into one call site.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| zod | npm | You are validating whole objects and want a schema plus inferred TypeScript types, not a bag of string predicates |
| valibot | npm | You want schema validation in a browser bundle and care about every kilobyte, since only the validators you import are included |
| libphonenumber-js | npm | Phone numbers are the thing you actually need, and you want parsing, normalization, and formatting rather than a locale regex |