Backend Node.js Course · Level 2
Validation (zod, class-validator)
On this page
1. Why early validation is necessary
- Without validation, invalid data (for example,
agesent as a string instead of a number) flows through the entire business logic, wastes a network round-trip to the DB, and is only rejected by the DB at the final step due to a column data type mismatch. - If the route handler is not wrapped in
try/catch, the DB error will be returned to the client as a generic, ambiguous500 Internal Server Error, potentially leaking internal details (table names, DB driver) in the stack trace. - 3 core reasons to validate early, as soon as the request arrives (before business logic/DB):
- Accurate status codes, proper UX: user input errors should return
400 Bad Request, clearly specifying which field is invalid, rather than a vague500. - Security: prevents leaking internal system details via
500database errors. - Performance: avoids wasting computation on business logic and a DB round-trip for data that was invalid from the start.
- Accurate status codes, proper UX: user input errors should return
- Validation is typically implemented as middleware, running immediately after
express.json()and before reaching the actual route handler.
2. zod (schema validation, functional style)
- TypeScript types (
interface/type) exist only at compile time and are completely stripped at runtime, offering no validation for incoming request payloads.zodbridges this gap by providing runtime validation. - Defining a schema using builder functions:
js
const createUserSchema = z.object({
email: z.string().email(),
age: z.number().min(18),
});
z.infer<typeof schema>automatically generates TypeScript types from the schema, avoiding duplicate manualinterfacedeclarations that can easily drift out of sync when updated in one place but forgotten in another. The zod schema becomes the single source of truth.- Applied via shared middleware (factory pattern), leveraging
next(err)/next()learned in the Middleware section:
js
function validate(schema) {
return (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.issues });
}
req.body = result.data;
next();
};
}
app.post('/users', validate(createUserSchema), (req, res) => {
// req.body is validated and strongly typed
});
3. class-validator (decorator, OOP style, common in NestJS)
- Unlike
interface/type, aclassin TypeScript (along with decorators attached to it) compiles into real JS and persists at runtime.class-validatorreads metadata from decorators (viareflect-metadata) to validate class instances at runtime.
ts
class CreateUserDto {
@IsEmail()
email: string;
@IsNumber()
@Min(18)
age: number;
}
- In NestJS, manual validation middleware is unnecessary: the built-in
ValidationPipeautomatically applies to any DTO class containing decorators:
ts
@Post('users')
createUser(@Body() dto: CreateUserDto) {
// dto is validated automatically by ValidationPipe before reaching this point
}
4. Comparison: zod vs class-validator
| zod | class-validator | |
|---|---|---|
| Definition approach | Schema builder (z.object({...})) | Decorators on classes |
| TypeScript types | Generated from schema via z.infer<typeof schema> | The class itself (acts as both a compile-time type and a runtime object) |
| Integration approach | Manually write validate(schema) middleware (Express) | NestJS provides built-in ValidationPipe, applied automatically |
| Paradigm | Functional | OOP (classes, decorators) |
| Best suited for | Plain Express, or functional programming style preferences | NestJS (recommended by default in official docs) |
