DTO validator
Heads up: the legacy
validateDTO()middleware described in earlier docs has been replaced in v4 by the unified Validation system, which works with class-validator, Zod, Yup, or any custom validator adapter you register. The patterns on this page show the v4 way to keep using class-validator-style DTOs.
DTO pattern recap
A Data Transfer Object standardises payload shapes as they cross HTTP boundaries. v4 keeps the same DTO mindset but lets you choose the schema language:
- class-validator + class-transformer: decorator-based, classes (the v3 pattern, still supported).
- Zod: schema-first, fully type-inferred.
- Yup: schema-first with chainable builders.
- Custom adapter: implement
IValidatorand register it in the validation registry.
For a side-by-side feature comparison, see the Validation reference. This page focuses on the class-validator path because it's the closest thing to the legacy v3 flow.
Install class-validator
npm install class-validator class-transformer
The validation system loads class-validator lazily, so you only pay the cost when a controller actually uses it.
Define a DTO class
import { IsEmail, IsString, MinLength } from "class-validator";
export class CreateUserDTO {
@IsString()
@MinLength(2)
name!: string;
@IsEmail()
email!: string;
@IsString()
@MinLength(8)
password!: string;
}
Validate inside a controller (v4 way)
Use @validatedBody() from @expressots/adapter-express. It runs the registered validator on the incoming payload, formats helpful error messages, and rejects malformed requests with a 400 response: all without you wiring a guard or writing a try/catch:
import { controller, Post, validatedBody } from "@expressots/adapter-express";
import { provide } from "@expressots/core";
import { CreateUserDTO } from "./create-user.dto";
@controller("/users")
@provide(UsersController)
export class UsersController {
@Post("/")
create(@validatedBody(CreateUserDTO) dto: CreateUserDTO) {
// dto is fully typed AND already validated.
// class-validator decorators run before this method is called.
return { ok: true, ...dto };
}
}
The same parameter decorator is also exported as @validatedQuery, @validatedParam, and @validatedHeaders for the other request locations.
Programmatic validation
When you need to validate something outside an HTTP request: a queue payload, a CLI input, an event body: use ValidationService.validate() directly:
import { inject, ValidationService } from "@expressots/core";
@provide(ImportService)
export class ImportService {
constructor(
@inject(ValidationService) private readonly validator: ValidationService,
) {}
async importBatch(rows: unknown[]) {
for (const row of rows) {
const result = await this.validator.validate(row, CreateUserDTO);
if (!result.valid) throw new ValidationErrorClass(result.errors);
}
}
}
What about Zod / Yup?
Same controller, different schema:
import { z } from "zod";
import { createZodValidator } from "@expressots/core";
import { controller, Post, validatedBody } from "@expressots/adapter-express";
const CreateUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
password: z.string().min(8),
});
const createUserValidator = createZodValidator(CreateUserSchema);
@controller("/users")
@provide(UsersController)
export class UsersController {
@Post("/")
create(@validatedBody(createUserValidator) dto: z.infer<typeof CreateUserSchema>) {
return { ok: true, ...dto };
}
}
createYupValidator(schema) is the equivalent for Yup.
See also
- Validation: full reference for the validation system, registry, adapters, and error format.
- Configuration: typed environment variable validation with
defineConfig+Env.*. - Error handling: how validation errors are mapped to RFC 7807 problem details.