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: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
// }

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.

For complete security implementation, check out how to sanitize inputs in Node.js and how to use Joi for validation in Node.js.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team