celebrate vs express-validator vs joi vs yup
Data Validation Strategies: Server-Side vs Client-Side in JavaScript
celebrateexpress-validatorjoiyupSimilar Packages:

Data Validation Strategies: Server-Side vs Client-Side in JavaScript

joi, yup, celebrate, and express-validator are essential tools for ensuring data integrity in JavaScript applications, but they serve different layers of the stack. joi is a powerful, schema-based validation library originally built for Node.js backends, known for its rich API and strict type checking. yup is a similar schema builder designed specifically for the browser and React ecosystems, focusing on lightweight bundle sizes and form integration. celebrate acts as a middleware wrapper that brings joi's validation power directly into Express.js routes, handling request parsing and error formatting automatically. express-validator is a dedicated suite of middleware for Express that provides a chained API for sanitizing and validating request data without external schema dependencies. Together, these tools cover the full spectrum from raw input sanitization to complex nested object validation.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
celebrate01,34827.8 kB13 years agoMIT
express-validator06,237146 kB833 months agoMIT
joi021,1881.89 MB199a month agoBSD-3-Clause
yup023,673270 kB24610 months agoMIT

Data Validation Strategies: Server-Side vs Client-Side in JavaScript

Building reliable web applications means never trusting user input. Whether you are handling a login form on the frontend or processing JSON payloads in an API, you need tools to check data shape, type, and content. The JavaScript ecosystem offers four major players for this task: joi, yup, celebrate, and express-validator. While they all validate data, they solve different problems in different parts of your stack. Let's break down how they work, where they shine, and how to pick the right one for your architecture.

πŸ—οΈ Core Philosophy: Schema Builders vs Middleware Chains

The first major split is between libraries that define schemas (blueprints for your data) and those that act as middleware (functions that run during a request).

joi and yup are schema builders. You define what your data should look like once, and then you use that definition to check data anywhere. This approach is great for consistency because your validation rules live in a single place.

express-validator and celebrate are middleware-focused. They are designed to sit inside your web server's request pipeline. While celebrate uses joi schemas under the hood, express-validator uses its own chainable functions to define rules right inside your route handlers.

πŸ” Defining Rules: Declarative Schemas

When you need to validate complex nested objects, declarative schemas make your code readable and reusable.

joi uses a very verbose but powerful API. It allows for deep nesting, conditional rules, and custom messages.

const Joi = require('joi');

const userSchema = Joi.object({
  username: Joi.string().alphanum().min(3).max(30).required(),
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(18),
  role: Joi.string().valid('admin', 'user').default('user')
});

const { error, value } = userSchema.validate({ username: 'abc', email: 'bad' });
if (error) console.log(error.message);

yup looks almost identical to joi but is built to be lighter. It supports similar chaining methods, making it easy to switch between them if needed.

import * as yup from 'yup';

const userSchema = yup.object({
  username: yup.string().alphanum().min(3).max(30).required(),
  email: yup.string().email().required(),
  age: yup.number().integer().min(18),
  role: yup.string().oneOf(['admin', 'user']).default('user')
});

try {
  await userSchema.validate({ username: 'abc', email: 'bad' });
} catch (err) {
  console.log(err.message);
}

express-validator does not use a separate schema object. Instead, you chain validation functions directly in your route definition. This keeps the validation logic close to the route but can get messy if the rules are complex.

const { body, validationResult } = require('express-validator');

app.post('/user', 
  body('username').isAlphanumeric().isLength({ min: 3, max: 30 }),
  body('email').isEmail(),
  body('age').optional().isInt({ min: 18 }),
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    next();
  }
);

celebrate lets you use joi schemas but maps them to specific parts of the HTTP request (body, query, params). You define the schema once and pass it to the middleware.

const { celebrate, Joi, segments } = require('celebrate');

app.post('/user',
  celebrate({
    [segments.BODY]: Joi.object({
      username: Joi.string().alphanum().min(3).max(30).required(),
      email: Joi.string().email().required()
    })
  }),
  (req, res) => {
    // If code reaches here, validation passed
    res.send('User created');
  }
);

🌐 Where They Run: Browser vs Server

This is the most critical architectural decision. Not all validation libraries work everywhere.

joi is heavy. It includes features for parsing dates, binaries, and complex types that rely on Node.js internals or large polyfills. While it can run in the browser, it will bloat your bundle size significantly. It is best kept on the server.

yup was built for the browser. It strips out heavy dependencies and focuses on the types you usually see in forms (strings, numbers, booleans, arrays). It is the go-to choice for React, Vue, or Svelte apps.

express-validator and celebrate are Express middleware. They only run on the server (Node.js). They cannot be used in the browser because they depend on the req and res objects from Express.

πŸ›‘οΈ Error Handling and Feedback

How a library reports errors matters for both API responses and form UIs.

joi returns an error object with a detailed path array. This is great for mapping errors back to specific fields in a deep object.

// Joi error structure
// error.details: [{ message: '"email" is required', path: ['email'] }]

yup throws a ValidationError that also contains a path. Form libraries like Formik know how to catch this specific error type and map it to form fields automatically.

// Yup error structure
// err.path: 'email'
// err.message: 'email is a required field'

express-validator collects errors in a Result object. You must manually call .array() or .mapped() to format them for your API response. This gives you control but requires extra code.

const errors = validationResult(req);
if (!errors.isEmpty()) {
  // Returns [{ msg: 'Invalid email', param: 'email', location: 'body' }]
  return res.status(400).json({ errors: errors.array() });
}

celebrate automates this. If validation fails, it catches the joi error, formats it into a standard HTTP 400 response, and passes it to your Express error handler. You don't write any error-checking code in your routes.

// Celebrate automatically triggers your global error handler
// No need for if (!errors.isEmpty()) checks in every route
app.use((err, req, res, next) => {
  if (err.isJoi) {
    res.status(400).send(err.details);
  }
});

🧹 Sanitization: Cleaning Data

Validation checks if data is correct. Sanitization changes data to make it safe or consistent (like trimming spaces or escaping HTML).

express-validator shines here. It has built-in sanitizers chained right next to validators.

body('email')
  .trim() // Removes whitespace
  .normalizeEmail() // Converts to lowercase standard email
  .isEmail();

joi and yup can transform data (like converting a string to a number), but they are not designed for security sanitization like escaping XSS attacks. You usually need a separate library for that if you choose them.

celebrate relies on joi's transformation capabilities. It can coerce types (e.g., turn "123" into 123) but lacks the dedicated security sanitizers found in express-validator.

πŸ”„ Real-World Usage Patterns

Scenario 1: React Form with Formik

You are building a signup form. You need validation that shows errors instantly as the user types.

  • βœ… Best Choice: yup
  • Why? It integrates natively with Formik. The schema defines both the initial values structure and the validation rules.
const formik = useFormik({
  initialValues: { email: '' },
  validationSchema: yup.object({ email: yup.string().email().required() }),
  onSubmit: values => api.submit(values)
});

Scenario 2: Express API with Strict Contracts

You are building a public REST API. You want to ensure no bad data ever reaches your controller logic, and you want standard error messages.

  • βœ… Best Choice: celebrate
  • Why? It acts as a gatekeeper. If the data doesn't match the joi schema, the controller never runs. It reduces boilerplate in every route file.
app.post('/login', celebrate({ body: loginSchema }), loginController);

Scenario 3: Quick Internal Tool or Microservice

You need a simple endpoint that accepts a few fields, trims them, and saves to a database. You don't want to define separate schema files.

  • βœ… Best Choice: express-validator
  • Why? The inline chaining is fast to write and includes sanitization out of the box. Perfect for smaller services where strict schema reuse isn't a priority.
app.post('/save', 
  body('name').trim().notEmpty(), 
  saveHandler
);

Scenario 4: Shared Validation Logic (Monorepo)

You have a monorepo with a Node backend and a React frontend. You want to share the exact same validation rules.

  • βœ… Best Choice: yup (used on both sides)
  • Why? While joi can technically run in the browser, yup is optimized for it. You can export the schema from a shared package and import it in both the Express route (via celebrate or manual check) and the React form.

πŸ“Š Summary Table

Featurejoiyupcelebrateexpress-validator
Primary UseServer Schema ValidationClient Schema ValidationExpress Middleware for JoiExpress Middleware Chain
Bundle SizeLarge (Heavy)Small (Light)N/A (Server only)Medium (Server only)
Syntax StyleDeclarative SchemaDeclarative SchemaSchema + Middleware ConfigChainable Functions
SanitizationLimited (Coercion)Limited (Coercion)Limited (via Joi)Excellent (Built-in)
Error OutputDetailed Error ObjectThrow ValidationErrorAuto HTTP 400 ResponseManual Result Extraction
Best ForComplex Backend APIsReact Forms / FrontendStrict Express GatekeepingQuick Express Endpoints

πŸ’‘ Final Recommendation

Your choice depends on where the validation happens and how your team likes to organize code.

If you are on the frontend, yup is the clear winner. It is light, fast, and plays nicely with modern form libraries. Do not try to force joi into your browser bundle unless you have a very specific reason.

If you are on the backend with Express:

  • Use celebrate if you love joi's schema syntax and want to enforce strict contracts with zero boilerplate. It is the most robust option for large APIs.
  • Use express-validator if you prefer keeping validation logic inside your route files, need strong sanitization features, or want to avoid managing separate schema files for simple endpoints.
  • Use raw joi in your service layer if you are not using Express or need to validate data outside of the HTTP request cycle (e.g., validating messages from a queue).

By matching the tool to the layer of your application, you ensure your data is safe without over-engineering your codebase.

How to Choose: celebrate vs express-validator vs joi vs yup

  • celebrate:

    Choose celebrate if you are using Express.js and want to leverage joi for route validation without writing repetitive middleware boilerplate. It automatically extracts data from req.body, req.query, req.params, and req.headers, applying joi schemas and returning standardized error responses. This is the best choice for teams that want strict schema enforcement at the API gateway level with minimal setup.

  • express-validator:

    Choose express-validator if you prefer a lightweight, chainable API that lives entirely within the Express middleware stack without needing a separate schema definition language. It excels at quick input sanitization (like escaping HTML or trimming strings) alongside validation. It is suitable for projects that want to avoid the complexity of external schema builders or need fine-grained control over exactly where validation occurs in the middleware chain.

  • joi:

    Choose joi if you are building a Node.js backend API that requires robust, complex schema validation with extensive rules for nested objects and arrays. It is the industry standard for server-side validation where bundle size is not a concern and you need deep customization. Avoid using it directly in the browser due to its large footprint unless you specifically need its exact rule set on the client.

  • yup:

    Choose yup if you are working in a frontend React application, especially when paired with form libraries like Formik or React Hook Form. It offers a similar declarative API to joi but is optimized for browser environments with a smaller bundle size. It is ideal for scenarios where you need to share validation logic between server and client via a universal schema approach.

README for celebrate

celebrate

Current Version Build Status airbnb-style Code Coverage Total Downloads

celebrate is an express middleware function that wraps the joi validation library. This allows you to use this middleware in any single route, or globally, and ensure that all of your inputs are correct before any handler function. The middleware allows you to validate req.params, req.headers, and req.query.

The middleware will also validate:

celebrate lists joi as a formal dependency. This means that celebrate will always use a predictable, known version of joi during the validation and compilation steps. There are two reasons for this:

  1. To ensure that celebrate can always use the latest version of joi as soon as it's published
  2. So that celebrate can export the version of joi it uses to the consumer to maximize compatibility

express Compatibility

celebrate is tested and has full compatibility with express 4 and 5. It likely works correctly with express 3, but including it in the test matrix was more trouble than it's worth. This is primarily because express 3 exposes route parameters as an array rather than an object.

Example Usage

Example of using celebrate on a single POST route to validate req.body.

const express = require('express');
const BodyParser = require('body-parser');
const { celebrate, Joi, errors, Segments } = require('celebrate');

const app = express();
app.use(BodyParser.json());

app.post('/signup', celebrate({
  [Segments.BODY]: Joi.object().keys({
    name: Joi.string().required(),
    age: Joi.number().integer(),
    role: Joi.string().default('admin')
  }),
  [Segments.QUERY]: {
    token: Joi.string().token().required()
  }
}), (req, res) => {
  // At this point, req.body has been validated and 
  // req.body.role is equal to req.body.role if provided in the POST or set to 'admin' by joi
});
app.use(errors());

Example of using celebrate to validate all incoming requests to ensure the token header is present and matches the supplied regular expression.

const express = require('express');
const { celebrate, Joi, errors, Segments } = require('celebrate');
const app = express();

// validate all incoming request headers for the token header
// if missing or not the correct format, respond with an error
app.use(celebrate({
  [Segments.HEADERS]: Joi.object({
    token: Joi.string().required().regex(/abc\d{3}/)
  }).unknown()
}));
app.get('/', (req, res) => { res.send('hello world'); });
app.get('/foo', (req, res) => { res.send('a foo request'); });
app.use(errors());

API

celebrate does not have a default export. The following methods encompass the public API.

celebrate(schema, [joiOptions], [opts])

Returns a function with the middleware signature ((req, res, next)).

  • requestRules - an object where key can be one of the values from Segments and the value is a joi validation schema. Only the keys specified will be validated against the incoming request object. If you omit a key, that part of the req object will not be validated. A schema must contain at least one valid key.
  • [joiOpts] - optional object containing joi options that are passed directly into the validate function. Defaults to { warnings: true }.
  • [opts] - an optional object with the following keys. Defaults to {}.
    • reqContext - bool value that instructs joi to use the incoming req object as the context value during joi validation. If set, this will trump the value of joiOptions.context. This is useful if you want to validate part of the request object against another part of the request object. See the tests for more details.
    • mode - optional Modes for controlling the validation mode celebrate uses. Defaults to partial.

celebrator([opts], [joiOptions], schema)

This is a curried version of celebrate. It is curried with lodash.curryRight so it can be called in all the various fashions that API supports. Returns a function with the middleware signature ((req, res, next)).

  • [opts] - an optional object with the following keys. Defaults to {}.
    • reqContext - bool value that instructs joi to use the incoming req object as the context value during joi validation. If set, this will trump the value of joiOptions.context. This is useful if you want to validate part of the request object against another part of the request object. See the tests for more details.
    • mode - optional Modes for controlling the validation mode celebrate uses. Defaults to partial.
  • [joiOpts] - optional object containing joi options that are passed directly into the validate function. Defaults to { warnings: true }.
  • requestRules - an object where key can be one of the values from Segments and the value is a joi validation schema. Only the keys specified will be validated against the incoming request object. If you omit a key, that part of the req object will not be validated. A schema must contain at least one valid key.
Sample usage

This is an example use of curried celebrate in a real server.

  const express = require('express');
  const { celebrator, Joi, errors, Segments } = require('celebrate');
  const app = express();

  // now every instance of `celebrate` will use these same options so you only
  // need to do it once.
  const celebrate = celebrator({ reqContext: true }, { convert: true });

  // validate all incoming request headers for the token header
  // if missing or not the correct format, respond with an error
  app.use(celebrate({
    [Segments.HEADERS]: Joi.object({
      token: Joi.string().required().regex(/abc\d{3}/)
    }).unknown()
  }));
  app.get('/', celebrate({
    [Segments.HEADERS]: Joi.object({
      name: Joi.string().required()
    })
  }), (req, res) => { res.send('hello world'); });
  app.use(errors());

Here are some examples of other ways to call celebrator

  const opts = { reqContext: true };
  const joiOpts = { convert: true };
  const schema = {
    [Segments.HEADERS]: Joi.object({
      name: Joi.string().required()
    })
  };

  let c = celebrator(opts)(joiOpts)(schema);
  c = celebrator(opts, joiOpts)(schema);
  c = celebrator(opts)(joiOpts, schema);
  c = celebrator(opts, joiOpts, schema);

  // c would function the same in all of these cases.

errors([opts])

Returns a function with the error handler signature ((err, req, res, next)). This should be placed with any other error handling middleware to catch celebrate errors. If the incoming err object is an error originating from celebrate, errors() will respond a pre-build error object. Otherwise, it will call next(err) and will pass the error along and will need to be processed by another error handler.

  • [opts] - an optional object with the following keys
    • statusCode - number that will be used for the response status code in the event of an error. Must be greater than 399 and less than 600. It must also be a number available to the node HTTP module. Defaults to 400.
    • message - string that will be used for the message value sent out by the error handler. Defaults to 'Validation failed'

If the error response format does not suite your needs, you are encouraged to write your own and check isCelebrateError(err) to format celebrate errors to your liking.

Errors origintating from the celebrate() middleware are CelebrateError objects.

Joi

celebrate exports the version of joi it is using internally. For maximum compatibility, you should use this version when creating schemas used with celebrate.

Segments

An enum containing all the segments of req objects that celebrate can validate against.

{
  BODY: 'body',
  COOKIES: 'cookies',
  HEADERS: 'headers',
  PARAMS: 'params',
  QUERY: 'query',
  SIGNEDCOOKIES: 'signedCookies',
}

Modes

An enum containing all the available validation modes that celebrate can support.

  • PARTIAL - ends validation on the first failure. Does not apply joi transformations if any part of the request is invalid.
  • FULL - validates the entire request object and collects all the validation failures in the result. Does not apply joi transformations if any part of the request is invalid.
    • Note: In order for this to work, you will need to pass abortEarly: false to #joiOptions. Or to get the default behavior along with this, { abortEarly: false, warnings: true }

new CelebrateError([message], [opts])

Creates a new CelebrateError object. Extends the built in Error object.

  • message - optional string message. Defaults to 'Validation failed'.
  • [opts] - optional object with the following keys
    • celebrated - bool that, when true, adds Symbol('celebrated'): true to the result object. This indicates this error as originating from celebrate. You'd likely want to set this to true if you want the celebrate error handler to handle errors originating from the format function that you call in user-land code. Defaults to false.

CelebrateError has the following public properties:

  • details - a Map of all validation failures. The key is a Segments and the value is a joi validation error. Adding to details is done via details.set. The value must be a joi validation error or an exception will be thrown.
Sample usage
  const result = Joi.validate(req.params.id, Joi.string().valid('foo'), { abortEarly: false });
  const err = new CelebrateError(undefined, { celebrated: true });
  err.details.set(Segments.PARAMS, result.error);

isCelebrateError(err)

Returns true if the provided err object originated from the celebrate middleware, and false otherwise. Useful if you want to write your own error handler for celebrate errors.

  • err - an error object

Additional Details

Validation Order

celebrate validates request values in the following order:

  1. req.headers
  2. req.params
  3. req.query
  4. req.cookies (assuming cookie-parser is being used)
  5. req.signedCookies (assuming cookie-parser is being used)
  6. req.body (assuming body-parser is being used)

Mutation Warning

If you use any of joi's updating validation APIs (default, rename, etc.) celebrate will override the source value with the changes applied by joi (assuming the request is valid).

For example, if you validate req.query and have a default value in your joi schema, if the incoming req.query is missing a value for default, during validation celebrate will overwrite the original req.query with the result of joi.validate. This is done so that once req has been validated, you can be sure all the inputs are valid and ready to consume in your handler functions and you don't need to re-write all your handlers to look for the query values in res.locals.*.

Additional Info

According the the HTTP spec, GET requests should not include a body in the request payload. For that reason, celebrate does not validate the body on GET requests.

Issues

Before opening issues on this repo, make sure your joi schema is correct and working as you intended. The bulk of this code is just exposing the joi API as express middleware. All of the heavy lifting still happens inside joi. You can go here to verify your joi schema easily.