express-joi-validation vs express-validator
Request Validation Strategies in Express.js Applications
express-joi-validationexpress-validatorSimilar Packages:

Request Validation Strategies in Express.js Applications

express-joi-validation and express-validator are both middleware solutions designed to validate incoming HTTP requests in Express.js applications, but they rely on different underlying validation libraries and philosophies. express-joi-validation acts as a wrapper around joi, a powerful schema description language and validator for JavaScript objects, focusing on strict schema definition. express-validator is built on top of validator.js, offering a chainable API that integrates closely with Express middleware patterns, allowing for validation and sanitization in a single flow. Both aim to prevent invalid data from reaching business logic, yet they differ significantly in syntax, error handling structures, and flexibility regarding schema reuse.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
express-joi-validation010321.9 kB10a year agoMIT
express-validator06,239146 kB824 months agoMIT

Request Validation in Express: express-joi-validation vs express-validator

Validating incoming data is one of the most critical security and stability layers in any Node.js API. Without it, your database might store garbage, your logic might crash, and your users might exploit type confusion. In the Express ecosystem, express-joi-validation and express-validator are two common choices, but they solve the problem in fundamentally different ways. Let's look at how they compare in real engineering scenarios.

๐Ÿ“˜ Validation Philosophy: Schema Objects vs Chainable Middleware

express-joi-validation relies on joi, which uses a declarative schema object approach. You define what your data should look like once, and the middleware enforces it. This feels like writing a contract.

// express-joi-validation: Define a schema object
const schema = createValidator({
  body: Joi.object({
    username: Joi.string().required(),
    age: Joi.number().min(18)
  })
});

app.post('/user', schema, (req, res) => {
  // req.body is guaranteed to match schema
});

express-validator uses a chainable API that lives directly in your route definition. You build validation rules step-by-step using functions. This feels like writing instructions.

// express-validator: Chain rules in the route
app.post('/user', [
  body('username').isString().notEmpty(),
  body('age').isInt({ min: 18 })
], (req, res) => {
  // Validation results attached to request
});

๐Ÿงน Sanitization: External vs Built-In

express-joi-validation focuses strictly on validation. If you need to clean data (like trimming whitespace or escaping HTML), you usually need to do it separately or rely on joi's limited transform features. It keeps concerns separate but can require more boilerplate for cleaning.

// express-joi-validation: Validation only
const schema = createValidator({
  body: Joi.object({
    email: Joi.string().email().required()
    // Trimming must be handled manually or via Joi extensions
  })
});

express-validator includes sanitization methods right alongside validation rules. You can trim, escape, and normalize data in the same chain where you validate it. This reduces the number of middleware layers you need to manage.

// express-validator: Validation + Sanitization
app.post('/user', [
  body('email').trim().normalizeEmail().isEmail(),
  body('username').trim().escape()
], (req, res) => {
  // Data is cleaned and validated
});

โŒ Error Handling: Custom Handlers vs Standardized Results

express-joi-validation passes validation errors to a specific error handling middleware. The errors are detailed joi error objects, which are rich but can be verbose. You often need a wrapper to format them for your API response.

// express-joi-validation: Error middleware
app.use((err, req, res, next) => {
  if (err.type === 'body.validation') {
    return res.status(400).json({ errors: err.details });
  }
  next(err);
});

express-validator attaches validation results directly to the request object (req). You check for errors explicitly in your route or use a standardized result formatter. This gives you more control over when and how to respond to failures.

// express-validator: Check results in route
app.post('/user', [
  body('email').isEmail()
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
});

๐Ÿ”„ Schema Reusability: High vs Moderate

express-joi-validation excels at reusability. Since joi schemas are standalone objects, you can import them into multiple routes, share them with frontend teams for type documentation, or use them in testing suites without pulling in Express dependencies.

// express-joi-validation: Reusable schema
// schemas/user.js
export const userSchema = Joi.object({ /* ... */ });

// routes/user.js
import { userSchema } from '../schemas/user';
app.post('/user', createValidator({ body: userSchema }), handler);

express-validator encourages defining rules close to the route. While you can extract arrays of middleware into separate files, the tight coupling to Express's req object makes it slightly harder to use outside of an HTTP context (like validating a message queue payload).

// express-validator: Middleware arrays
// middleware/validation.js
export const validateUser = [
  body('email').isEmail(),
  body('password').isLength({ min: 6 })
];

// routes/user.js
app.post('/user', validateUser, handler);

๐Ÿ› ๏ธ Maintenance and Ecosystem Status

express-joi-validation has seen periods of low activity. While joi itself is actively maintained, this specific wrapper does not receive frequent updates. In modern architectures, many teams are moving towards using joi directly with custom middleware or switching to celebrate, which is a more actively maintained joi wrapper for Express.

// Note: Many teams now prefer 'celebrate' for Joi integration
// instead of express-joi-validation due to maintenance concerns
import { celebrate, Joi, Segments } from 'celebrate';

app.post('/user', celebrate({
  [Segments.BODY]: Joi.object({ /* ... */ })
}), handler);

express-validator is highly active and widely adopted. It receives regular updates to support new Express features and security patches. Its documentation is extensive, and finding solutions to common problems is easy due to its large user base.

// express-validator: Active ecosystem
// Regularly updated to match Express best practices
// Large community support for custom validators

๐Ÿค Similarities: Shared Ground

Despite their differences, both packages aim to secure your API entry points.

1. ๐Ÿ”’ Both Prevent Invalid Data

  • Stop bad requests before they hit your controller logic.
  • Return 400 Bad Request status codes automatically (with configuration).
// Both ensure this code never runs with invalid data
app.post('/transfer', validationMiddleware, (req, res) => {
  db.transfer(req.body.amount); // Safe to use
});

2. โš™๏ธ Both Support Custom Rules

  • You can write custom validation logic if built-in rules aren't enough.
  • Allows for complex business rule checks (e.g., "password cannot match username").
// express-joi-validation: Custom Joi extension
Joi.extend({ type: 'string', base: Joi.string(), messages: { /*...*/ } });

// express-validator: Custom validator function
body('password').custom((value, { req }) => {
  if (value === req.body.username) throw new Error('Match not allowed');
  return true;
});

3. ๐Ÿ“ฆ Both Work with Async Logic

  • Support asynchronous validation (e.g., checking if a username exists in the DB).
  • Integrate smoothly with Express async error handlers.
// express-validator: Async check
body('email').custom(async (value) => {
  const user = await User.findOne({ email: value });
  if (user) throw new Error('Email in use');
});

๐Ÿ“Š Summary: Key Differences

Featureexpress-joi-validationexpress-validator
Core LibraryWraps joiWraps validator.js
Syntax StyleDeclarative Schema ObjectsChainable Middleware Functions
SanitizationLimited / ManualBuilt-in & Chainable
Error AccessError Handling MiddlewarevalidationResult(req)
ReusabilityHigh (Schema outside routes)Moderate (Middleware arrays)
MaintenanceLow ActivityHigh Activity

๐Ÿ’ก The Big Picture

express-joi-validation is like a strict contract signer ๐Ÿ“. It's best if you love joi and want to define your data shapes once and use them everywhere. However, given its maintenance status, consider using celebrate if you want this same joi-based approach with better long-term support.

express-validator is like a Swiss Army knife ๐Ÿ”ช. It's versatile, actively maintained, and keeps validation logic close to your routes. It's the safer default choice for most new Express projects today, especially if you want built-in sanitization and a huge community backing.

Final Thought: If you are starting a new project today, express-validator is generally the more robust choice due to its active maintenance and integrated sanitization. If you are already invested in the joi ecosystem, look at celebrate as a modern alternative to express-joi-validation.

How to Choose: express-joi-validation vs express-validator

  • express-joi-validation:

    Choose express-joi-validation if your team already relies heavily on joi for schema validation across the stack (e.g., in frontend forms or other Node services) and you value strict, object-based schema definitions. It is suitable for projects where separating validation schemas from route handlers is a priority, promoting reusability and clear contracts. However, be aware that this package sees less frequent updates compared to alternatives, so verify its compatibility with your Express version before committing.

  • express-validator:

    Choose express-validator if you prefer a middleware-centric approach that combines validation and sanitization without needing an external schema library like joi. It is ideal for teams that want tight integration with Express's request/response cycle and appreciate the chainable syntax for defining rules directly within route files. This package is widely maintained and has a large ecosystem of examples, making it a safer bet for long-term support in standard Express applications.

README for express-joi-validation

express-joi-validation

TravisCI Coverage Status npm version TypeScript npm downloads Known Vulnerabilities

A middleware for validating express inputs using Joi schemas. Features include:

  • TypeScript support.
  • Specify the order in which request inputs are validated.
  • Replaces the incoming req.body, req.query, etc and with the validated result
  • Retains the original req.body inside a new property named req.originalBody. . The same applies for headers, query, and params using the original prefix, e.g req.originalQuery
  • Chooses sensible default Joi options for headers, params, query, and body.
  • Uses peerDependencies to get a Joi instance of your choosing instead of using a fixed version.

Quick Links

Install

You need to install joi with this module since it relies on it in peerDependencies.

npm i express-joi-validation joi --save

Example

A JavaScript and TypeScript example can be found in the example/ folder of this repository.

Usage (JavaScript)

const Joi = require('joi')
const app = require('express')()
const validator = require('express-joi-validation').createValidator({})

const querySchema = Joi.object({
  name: Joi.string().required()
})

app.get('/orders', validator.query(querySchema), (req, res) => {
  // If we're in here then the query was valid!  
  res.end(`Hello ${req.query.name}!`)
})

Usage (TypeScript)

For TypeScript a helper ValidatedRequest and ValidatedRequestWithRawInputsAndFields type is provided. This extends the express.Request type and allows you to pass a schema using generics to ensure type safety in your handler function.

import * as Joi from 'joi'
import * as express from 'express'
import {
  ContainerTypes,
  // Use this as a replacement for express.Request
  ValidatedRequest,
  // Extend from this to define a valid schema type/interface
  ValidatedRequestSchema,
  // Creates a validator that generates middlewares
  createValidator
} from 'express-joi-validation'

const app = express()
const validator = createValidator()

const querySchema = Joi.object({
  name: Joi.string().required()
})

interface HelloRequestSchema extends ValidatedRequestSchema {
  [ContainerTypes.Query]: {
    name: string
  }
}

app.get(
  '/hello',
  validator.query(querySchema),
  (req: ValidatedRequest<HelloRequestSchema>, res) => {
    // Woohoo, type safety and intellisense for req.query!
    res.end(`Hello ${req.query.name}!`)
  }
)

You can minimise some duplication by using joi-extract-type.

NOTE: this does not work with Joi v16+ at the moment. See this issue.

import * as Joi from 'joi'
import * as express from 'express'
import {
  // Use this as a replacement for express.Request
  ValidatedRequest,
  // Extend from this to define a valid schema type/interface
  ValidatedRequestSchema,
  // Creates a validator that generates middlewares
  createValidator
} from 'express-joi-validation'

// This is optional, but without it you need to manually generate
// a type or interface for ValidatedRequestSchema members
import 'joi-extract-type'

const app = express()
const validator = createValidator()

const querySchema = Joi.object({
  name: Joi.string().required()
})

interface HelloRequestSchema extends ValidatedRequestSchema {
  [ContainerTypes.Query]: Joi.extractType<typeof querySchema>

  // Without Joi.extractType you would do this:
  // query: {
  //   name: string
  // }
}

app.get(
  '/hello',
  validator.query(querySchema),
  (req: ValidatedRequest<HelloRequestSchema>, res) => {
    // Woohoo, type safety and intellisense for req.query!
    res.end(`Hello ${req.query.name}!`)
  }
)

API

Structure

createValidator(config)

Creates a validator. Supports the following options:

  • passError (default: false) - Passes validation errors to the express error hander using next(err) when true
  • statusCode (default: 400) - The status code used when validation fails and passError is false.

validator.query(schema, [options])

Creates a middleware instance that will validate the req.query for an incoming request. Can be passed options that override the config passed when the validator was created.

Supported options are:

  • joi - Custom options to pass to Joi.validate.
  • passError - Same as above.
  • statusCode - Same as above.

validator.body(schema, [options])

Creates a middleware instance that will validate the req.body for an incoming request. Can be passed options that override the options passed when the validator was created.

Supported options are the same as validator.query.

validator.headers(schema, [options])

Creates a middleware instance that will validate the req.headers for an incoming request. Can be passed options that override the options passed when the validator was created.

Supported options are the same as validator.query.

validator.params(schema, [options])

Creates a middleware instance that will validate the req.params for an incoming request. Can be passed options that override the options passed when the validator was created.

Supported options are the same as validator.query.

validator.response(schema, [options])

Creates a middleware instance that will validate the outgoing response. Can be passed options that override the options passed when the instance was created.

Supported options are the same as validator.query.

validator.fields(schema, [options])

Creates a middleware instance that will validate the fields for an incoming request. This is designed for use with express-formidable. Can be passed options that override the options passed when the validator was created.

The instance.params middleware is a little different to the others. It must be attached directly to the route it is related to. Here's a sample:

const schema = Joi.object({
  id: Joi.number().integer().required()
});

// INCORRECT
app.use(validator.params(schema));
app.get('/orders/:id', (req, res, next) => {
  // The "id" parameter will NOT have been validated here!
});

// CORRECT
app.get('/orders/:id', validator.params(schema), (req, res, next) => {
  // This WILL have a validated "id"
})

Supported options are the same as validator.query.

Behaviours

Joi Versioning

This module uses peerDependencies for the Joi version being used. This means whatever joi version is in the dependencies of your package.json will be used by this module.

Validation Ordering

Validation can be performed in a specific order using standard express middleware behaviour. Pass the middleware in the desired order.

Here's an example where the order is headers, body, query:

route.get(
  '/tickets',
  validator.headers(headerSchema),
  validator.body(bodySchema),
  validator.query(querySchema),
  routeHandler
);

Error Handling

When validation fails, this module will default to returning a HTTP 400 with the Joi validation error as a text/plain response type.

A passError option is supported to override this behaviour. This option forces the middleware to pass the error to the express error handler using the standard next function behaviour.

See the Custom Express Error Handler section for an example.

Joi Options

It is possible to pass specific Joi options to each validator like so:

route.get(
  '/tickets',
  validator.headers(
    headerSchema,
    {
      joi: {convert: true, allowUnknown: true}
    }
  ),
  validator.body(
    bodySchema,
    {
      joi: {convert: true, allowUnknown: false}
    }
  )
  routeHandler
);

The following sensible defaults for Joi are applied if none are passed:

Query

  • convert: true
  • allowUnknown: false
  • abortEarly: false

Body

  • convert: true
  • allowUnknown: false
  • abortEarly: false

Headers

  • convert: true
  • allowUnknown: true
  • stripUnknown: false
  • abortEarly: false

Route Params

  • convert: true
  • allowUnknown: false
  • abortEarly: false

Fields (with express-formidable)

  • convert: true
  • allowUnknown: false
  • abortEarly: false

Custom Express Error Handler

const validator = require('express-joi-validation').createValidator({
  // This options forces validation to pass any errors the express
  // error handler instead of generating a 400 error
  passError: true
});

const app = require('express')();
const orders = require('lib/orders');

app.get('/orders', validator.query(require('./query-schema')), (req, res, next) => {
  // if we're in here then the query was valid!
  orders.getForQuery(req.query)
    .then((listOfOrders) => res.json(listOfOrders))
    .catch(next);
});

// After your routes add a standard express error handler. This will be passed the Joi
// error, plus an extra "type" field so we can tell what type of validation failed
app.use((err, req, res, next) => {
  if (err && err.error && err.error.isJoi) {
    // we had a joi error, let's return a custom 400 json response
    res.status(400).json({
      type: err.type, // will be "query" here, but could be "headers", "body", or "params"
      message: err.error.toString()
    });
  } else {
    // pass on to another error handler
    next(err);
  }
});

In TypeScript environments err.type can be verified against the exported ContainerTypes:

import { ContainerTypes } from 'express-joi-validation'

app.use((err: any|ExpressJoiError, req: express.Request, res: express.Response, next: express.NextFunction) => {
  // ContainerTypes is an enum exported by this module. It contains strings
  // such as "body", "headers", "query"...
  if (err && 'type' in err && err.type in ContainerTypes) {
    const e: ExpressJoiError = err
    // e.g "You submitted a bad query paramater"
    res.status(400).end(`You submitted a bad ${e.type} paramater`)
  } else {
    res.status(500).end('internal server error')
  }
})