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.
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.
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
});
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
});
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() });
}
});
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);
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
Despite their differences, both packages aim to secure your API entry points.
// Both ensure this code never runs with invalid data
app.post('/transfer', validationMiddleware, (req, res) => {
db.transfer(req.body.amount); // Safe to use
});
// 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;
});
// express-validator: Async check
body('email').custom(async (value) => {
const user = await User.findOne({ email: value });
if (user) throw new Error('Email in use');
});
| Feature | express-joi-validation | express-validator |
|---|---|---|
| Core Library | Wraps joi | Wraps validator.js |
| Syntax Style | Declarative Schema Objects | Chainable Middleware Functions |
| Sanitization | Limited / Manual | Built-in & Chainable |
| Error Access | Error Handling Middleware | validationResult(req) |
| Reusability | High (Schema outside routes) | Moderate (Middleware arrays) |
| Maintenance | Low Activity | High Activity |
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.
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.
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.
A middleware for validating express inputs using Joi schemas. Features include:
req.body, req.query, etc and with the validated resultreq.body inside a new property named req.originalBody.
. The same applies for headers, query, and params using the original prefix,
e.g req.originalQuerypeerDependencies to get a Joi instance of your choosing instead of
using a fixed version.You need to install joi with this module since it relies on it in
peerDependencies.
npm i express-joi-validation joi --save
A JavaScript and TypeScript example can be found in the example/ folder of
this repository.
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}!`)
})
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}!`)
}
)
Creates a validator. Supports the following options:
false) - Passes validation errors to the express error
hander using next(err) when true400) - The status code used when validation fails and
passError is false.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.validate.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.
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.
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.
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.
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.
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 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
);
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.
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:
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')
}
})