Suggest an editImprove this articleRefine the answer for “How to validate request data in Express.js?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Request validation in Express.js** means checking `req.body`, `req.params`, and `req.query` against a schema before any business logic runs. Use Zod for TypeScript projects, Joi for plain JS. ```js const validate = (schema) => (req, res, next) => { const result = schema.safeParse(req.body); if (!result.success) return res.status(400).json({ errors: result.error.errors }); req.body = result.data; next(); }; app.post('/users', validate(userSchema), createUser); ``` **Key point:** always validate in middleware, before async operations.Shown above the full answer for quick recall.Answer (EN)Image**Request validation in Express.js** checks incoming data against predefined rules before it reaches your route handler, blocking bad input at the door. ## Theory ### TL;DR - Think of it like a bouncer at a club: the schema is the guest list, `req.body` is the person showing ID. No match, no entry. - The main approach: define a schema (Zod, Joi, or express-validator), wrap it in middleware, attach it to your route. - TypeScript project? Use Zod - it infers types automatically. Plain JS? Joi. Quick param or query checks only? express-validator is the lightest path. - Always put the validation middleware before any async work. Checking data after a DB call wastes resources on a request that was never valid. - All three libraries return structured, field-level errors - not just a generic 400. ### Quick example ```js const { z } = require('zod'); const express = require('express'); const app = express(); app.use(express.json()); const userSchema = z.object({ name: z.string().min(2), email: z.string().email() }); const validate = (schema) => (req, res, next) => { try { schema.parse(req.body); next(); } catch (err) { res.status(400).json({ errors: err.errors }); } }; app.post('/users', validate(userSchema), (req, res) => { res.json({ message: 'User created', data: req.body }); }); // { "name": "Bob", "email": "bob@example.com" } -> 200 OK // { "name": "A", "email": "invalid" } -> 400 with field-level errors ``` `validate()` is a higher-order function that returns middleware. The schema runs before the handler. If anything fails, the request stops there - the route handler never executes. ### Schema-based vs manual validation Manual `if` checks work fine for one or two fields. They fall apart fast after that: the same checks get copied across routes, edge cases around type coercion get missed, and gaps like prototype pollution can slip through. Schema libraries centralize the rules in one object, coerce types automatically, and produce structured error messages you can send directly to the client. Zod also generates TypeScript types from your schema, so `req.body` becomes fully typed downstream without separate interfaces. That alone cuts a lot of boilerplate in production TypeScript projects. ### When to use each library - **Zod**: TypeScript project, new codebase, want inferred types with no duplication. - **Joi**: Plain JavaScript, legacy app, or a team already familiar with Hapi's ecosystem. Slightly faster than Zod at very high throughput. - **express-validator**: Minimal setup, just a few URL params or query strings to check, no full schema library needed. - **class-validator**: NestJS or class-based architecture, decorator syntax on DTOs. ### Comparison table | Library | TS support | Error format | Performance | Best for | |---|---|---|---|---| | **Zod** | Native (inferred types) | Rich, path-based | Fast | New TS projects | | **Joi** | Manual types | Detailed | Fastest | Plain JS / legacy | | **express-validator** | None | Basic | Lightweight | Quick param checks | | **class-validator** | Decorators | Customizable | Slower (reflection) | NestJS / class-based | ### How the middleware stack handles this `express.json()` parses the raw HTTP body into `req.body` using Node's `http.IncomingMessage`. Your validation middleware runs next in the stack. Zod's `schema.parse()` walks the object recursively, checking types and constraints in sequence. On failure it throws a `ZodError`. Your middleware catches it and sends 400 before the route handler ever runs. One thing that trips people up consistently: `req.query` values are always strings. A query like `?page=2` gives you the string `"2"`, not the number `2`. Zod's `.transform(Number)` handles this, but you have to add it explicitly - it does not happen automatically. ### Common mistakes **Mistake 1: validating inside the handler, after async work.** ```js // Wrong: DB call runs even on bad input app.post('/users', async (req, res) => { await db.connect(); schema.parse(req.body); // too late }); // Fix: middleware runs first app.post('/users', validate(schema), async (req, res) => { await db.connect(); }); ``` **Mistake 2: forgetting `express.json()`.** Without it, `req.body` is `undefined`, and `schema.parse(undefined)` produces a confusing error instead of a clean 400. Add it before your routes with a size limit: ```js app.use(express.json({ limit: '10kb' })); ``` **Mistake 3: using `.optional()` on fields that should be required.** ```js // Wrong: passes empty strings and null name: z.string().optional() // Fix: require a non-empty string name: z.string().min(1) ``` This is a common source of DB constraint errors that appear much later in the request lifecycle, far from where the actual bug is. **Mistake 4: not transforming query string numbers.** ```js // Wrong: "abc" passes z.string() but breaks downstream logic query: z.object({ page: z.string() }) // Fix: transform and validate the number query: z.object({ page: z.string().transform(Number).refine(n => !isNaN(n) && n > 0, 'Must be a positive number') }) ``` ### Real-world usage - **Next.js API routes**: Zod on `/api/users` with inferred handler types, standard in Vercel templates. - **NestJS**: class-validator decorators on DTOs, used in most enterprise setups. - **Strapi CMS**: Joi schemas for custom plugin endpoints. - **Plain Express APIs**: express-validator for quick param checks, Zod for anything with a complex body structure. ### Follow-up questions **Q:** Why use middleware instead of validating inside the handler? **A:** Middleware runs before any business logic, saves CPU on invalid requests, and lets you reuse the same schema across multiple routes. It also composes cleanly with auth middleware in the same chain. **Q:** What is the difference between `parse()` and `safeParse()` in Zod? **A:** `parse()` throws a `ZodError` on failure - use it in try/catch inside middleware. `safeParse()` returns `{ success: boolean, data | error }` and never throws, which is better for WebSocket handlers or non-HTTP contexts where throwing is awkward. **Q:** How do you handle file uploads with validation? **A:** Use Multer to parse the multipart body first, then validate `req.body` and `req.file` with Zod. Check the MIME type with `z.enum(['image/jpeg', 'image/png'])` on `req.file.mimetype` after Multer runs. **Q:** Does Zod protect against prototype pollution? **A:** Yes, since Zod 3.22 keys like `__proto__` are stripped automatically. Joi removes unknown fields by default too. If you want to reject unexpected fields entirely rather than strip them, use `.strict()`. **Q (senior):** You are building an API gateway for a microservices system with 100 schemas loaded dynamically per route. How do you design the validation layer? **A:** Keep a central schema registry (a `Map` in-process, Redis for distributed setups) keyed by route ID. Load schemas lazily and cache the compiled Zod objects. Use an OpenAPI-to-Zod generator to keep schemas consistent with your API contracts. For non-TS services, fall back to Joi. Version schemas with a key like `users:v1` and `users:v2` so old and new routes coexist without conflict. ## Examples ### Basic: single body validation with safeParse ```js const { z } = require('zod'); const express = require('express'); const app = express(); app.use(express.json()); const loginSchema = z.object({ email: z.string().email(), password: z.string().min(8) }); const validate = (schema) => (req, res, next) => { const result = schema.safeParse(req.body); if (!result.success) { return res.status(400).json({ errors: result.error.errors }); } req.body = result.data; // use the coerced, validated data next(); }; app.post('/login', validate(loginSchema), (req, res) => { res.json({ message: 'Credentials valid' }); }); // { email: "a@b.com", password: "secret123" } -> 200 // { email: "notanemail", password: "short" } -> 400 with two field errors ``` `safeParse()` is cleaner than `parse()` in middleware because it never throws - the error is just a value you check. Assigning `result.data` back to `req.body` means the handler gets the coerced output, not the raw input. ### Intermediate: body and query params on a signup route ```js const createUserSchema = z.object({ body: z.object({ name: z.string().min(2).max(50), email: z.string().email(), password: z.string().min(8).regex( /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, 'Password must include upper, lower, and digit' ) }), query: z.object({ referral: z.string().optional() }) }); const validate = (schema) => (req, res, next) => { const result = schema.safeParse({ body: req.body, query: req.query }); if (!result.success) { return res.status(400).json({ error: 'Validation failed', details: result.error.errors.map(e => ({ field: e.path.join('.'), message: e.message })) }); } next(); }; app.post('/users', validate(createUserSchema), async (req, res) => { res.json({ userId: 123 }); }); // POST /users?referral=abc { name:"Alice", email:"a@b.com", password:"Pass123" } -> 200 // POST /users { name:"A", email:"bad", password:"weak" } -> 400, 3 field errors ``` Wrapping both `body` and `query` in one schema means a single middleware call covers the whole request. Each error object in the response includes the full field path like `body.password`, so the client knows exactly what failed. ### Advanced: nested arrays with transform This pattern trips up a lot of developers because Zod's `.transform()` changes the output shape, which affects what `req.body` looks like inside the handler. ```js const orderSchema = z.object({ items: z.array(z.object({ productId: z.string().uuid(), quantity: z.number().int().positive().max(99), metadata: z.record(z.any()).optional() // allows extra fields without breaking })) .min(1) .max(10) }).transform(data => ({ ...data, totalItems: data.items.reduce((sum, item) => sum + item.quantity, 0) })); const validate = (schema) => (req, res, next) => { const result = schema.safeParse(req.body); if (!result.success) { return res.status(400).json({ errors: result.error.errors }); } req.body = result.data; // includes totalItems computed by Zod next(); }; app.post('/orders', validate(orderSchema), (req, res) => { console.log(req.body.totalItems); // already computed, no extra logic needed res.json({ order: req.body }); }); // { items: [{ productId: "550e8400-e29b-41d4-a716-446655440001", quantity: 5 }] } // -> 200, totalItems: 5 // { items: [] } // -> 400 "Array must contain at least 1 item(s)" // { items: [{ productId: "not-uuid", quantity: 0 }] } // -> 400 two separate field errors ``` The `.transform()` step runs only after all validation passes. You get a clean, enriched object in `req.body` instead of computing `totalItems` again in the handler. Zod 3.22+ also strips `__proto__` and similar keys automatically, so prototype pollution is handled without any extra code.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.