How to validate data in Node.js
Data validation ensures user input meets expected format, type, and constraints before processing, preventing bugs and security vulnerabilities. As the creator of CoreUI with 12 years of Node.js development experience, I’ve implemented validation strategies in applications serving millions of users, catching invalid data at API boundaries and providing clear error messages that improve user experience while protecting against malicious input.
The most reliable approach uses validation libraries like Joi or Yup for schema-based validation.
Basic Manual Validation
function validateUser(user) {
const errors = []
if (!user.email || typeof user.email !== 'string') {
errors.push('Email is required and must be a string')
}
if (!user.age || typeof user.age !== 'number') {
errors.push('Age is required and must be a number')
}
if (user.age < 18) {
errors.push('Age must be at least 18')
}
if (errors.length > 0) {
throw new Error(errors.join(', '))
}
return true
}
// Usage
try {
validateUser({ email: '[email protected]', age: 25 })
console.log('Valid user')
} catch (error) {
console.error('Validation failed:', error.message)
}
Validation with Joi
npm install joi
const Joi = require('joi')
// Define schema
const userSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
age: Joi.number().integer().min(18).max(120),
username: Joi.string().alphanum().min(3).max(30).required(),
role: Joi.string().valid('user', 'admin', 'moderator').default('user')
})
// Validate data
function validateUser(data) {
const { error, value } = userSchema.validate(data, {
abortEarly: false // Return all errors, not just first
})
if (error) {
const errors = error.details.map(detail => detail.message)
throw new Error(errors.join(', '))
}
return value
}
// Usage
try {
const validUser = validateUser({
email: '[email protected]',
password: 'SecurePass123',
age: 25,
username: 'johndoe'
})
console.log('Valid user:', validUser)
} catch (error) {
console.error('Validation failed:', error.message)
}
Express Middleware Validation
const express = require('express')
const Joi = require('joi')
const app = express()
app.use(express.json())
// Validation middleware
function validate(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true // Remove unknown properties
})
if (error) {
const errors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}))
return res.status(400).json({
success: false,
errors
})
}
req.body = value
next()
}
}
// Define schemas
const createUserSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
username: Joi.string().alphanum().min(3).max(30).required()
})
const updateUserSchema = Joi.object({
email: Joi.string().email(),
username: Joi.string().alphanum().min(3).max(30)
}).min(1) // At least one field required
// Use in routes
app.post('/api/users', validate(createUserSchema), (req, res) => {
res.json({ success: true, user: req.body })
})
app.patch('/api/users/:id', validate(updateUserSchema), (req, res) => {
res.json({ success: true, user: req.body })
})
app.listen(3000)
Nested Object Validation
const Joi = require('joi')
const addressSchema = Joi.object({
street: Joi.string().required(),
city: Joi.string().required(),
state: Joi.string().length(2).required(),
zipCode: Joi.string().pattern(/^\d{5}$/).required()
})
const userSchema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
address: addressSchema.required(),
contacts: Joi.array().items(
Joi.object({
type: Joi.string().valid('phone', 'email').required(),
value: Joi.string().required()
})
).min(1)
})
const data = {
name: 'John Doe',
email: '[email protected]',
address: {
street: '123 Main St',
city: 'New York',
state: 'NY',
zipCode: '10001'
},
contacts: [
{ type: 'phone', value: '555-0123' },
{ type: 'email', value: '[email protected]' }
]
}
const { error, value } = userSchema.validate(data)
Custom Validation Rules
const Joi = require('joi')
// Custom validator
const customJoi = Joi.extend((joi) => ({
type: 'string',
base: joi.string(),
messages: {
'string.strongPassword': '{{#label}} must contain uppercase, lowercase, number and special character'
},
rules: {
strongPassword: {
validate(value, helpers) {
if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/.test(value)) {
return helpers.error('string.strongPassword')
}
return value
}
}
}
}))
const userSchema = customJoi.object({
email: customJoi.string().email().required(),
password: customJoi.string().min(8).strongPassword().required()
})
const { error } = userSchema.validate({
email: '[email protected]',
password: 'weak'
})
console.log(error?.details[0].message)
// "password" must contain uppercase, lowercase, number and special character
Async Validation
const Joi = require('joi')
const userSchema = Joi.object({
email: Joi.string().email().required().external(async (value) => {
// Check if email exists in database
const exists = await db.query('SELECT * FROM users WHERE email = ?', [value])
if (exists.length > 0) {
throw new Error('Email already exists')
}
}),
username: Joi.string().required()
})
// Validate with async rules
try {
const value = await userSchema.validateAsync(
{ email: '[email protected]', username: 'newuser' },
{ abortEarly: false }
)
console.log('Valid:', value)
} catch (error) {
console.error('Validation failed:', error.message)
}
Query Parameter Validation
const express = require('express')
const Joi = require('joi')
const app = express()
function validateQuery(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.query, {
abortEarly: false,
stripUnknown: true
})
if (error) {
return res.status(400).json({
success: false,
errors: error.details.map(d => d.message)
})
}
req.query = value
next()
}
}
const listUsersQuerySchema = Joi.object({
page: Joi.number().integer().min(1).default(1),
limit: Joi.number().integer().min(1).max(100).default(20),
sort: Joi.string().valid('name', 'email', 'createdAt').default('createdAt'),
order: Joi.string().valid('asc', 'desc').default('desc'),
search: Joi.string().allow('').optional()
})
app.get('/api/users', validateQuery(listUsersQuerySchema), (req, res) => {
console.log('Validated query:', req.query)
// { page: 1, limit: 20, sort: 'createdAt', order: 'desc' }
res.json({ success: true, query: req.query })
})
app.listen(3000)
Sanitize and Validate
const Joi = require('joi')
const validator = require('validator')
const userSchema = Joi.object({
email: Joi.string().email().required().custom((value, helpers) => {
// Normalize email
return validator.normalizeEmail(value) || value
}),
website: Joi.string().uri().optional().custom((value, helpers) => {
// Ensure HTTPS
if (value && !value.startsWith('https://')) {
return helpers.error('string.httpsOnly')
}
return value
}),
bio: Joi.string().max(500).optional().custom((value, helpers) => {
// Escape HTML
return validator.escape(value)
})
}).messages({
'string.httpsOnly': 'Website must use HTTPS'
})
const { error, value } = userSchema.validate({
email: ' [email protected] ',
website: 'https://example.com',
bio: '<script>alert("xss")</script>'
})
console.log(value)
// {
// email: '[email protected]',
// website: 'https://example.com',
// bio: '<script>alert("xss")</script>'
// }
Best Practice Note
This is the validation strategy we use across all CoreUI Node.js applications. Joi provides schema-based validation with excellent error messages and type coercion. Always validate at API boundaries, use middleware for consistent validation across routes, sanitize data after validation, and provide clear error messages that help users fix invalid input. Combine validation with sanitization for maximum security.
For production applications, consider using CoreUI’s Node.js Admin Template which includes pre-configured validation middleware.
Related Articles
For complete security implementation, check out how to sanitize inputs in Node.js and how to use Joi for validation in Node.js.



