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
IValidationAdapterand register it in the validation config.
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;
}
Enable validation
Turn the validation system on once in your App class. The class-validator adapter is registered automatically:
import { AppExpress } from "@expressots/adapter-express";
export class App extends AppExpress {
async configureServices(): Promise<void> {
this.Middleware.addValidation();
}
}
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 a ValidationRegistry directly:
import { provide, ValidationRegistry, ClassValidatorAdapter } from "@expressots/core";
import { CreateUserDTO } from "./create-user.dto";
const registry = new ValidationRegistry();
registry.register(new ClassValidatorAdapter());
@provide(ImportService)
export class ImportService {
async importBatch(rows: Array<unknown>) {
for (const row of rows) {
const result = await registry.validate(row, CreateUserDTO);
if (!result.success) {
throw new Error(`Invalid row: ${JSON.stringify(result.errors)}`);
}
}
}
}
registry.validate() returns a ValidationResult: { success: true, data } on success, { success: false, errors } on failure, where errors is a ValidationFieldError[].
What about Zod / Yup?
Same controller, different schema. Register the Zod adapter once in your App class (this.Middleware.addValidation({ adapters: [ZodValidatorAdapter] })), then pass the Zod schema straight to the decorator:
import { z } from "zod";
import { provide } 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),
});
@controller("/users")
@provide(UsersController)
export class UsersController {
@Post("/")
create(@validatedBody(CreateUserSchema) dto: z.infer<typeof CreateUserSchema>) {
return { ok: true, ...dto };
}
}
For Yup, register YupValidatorAdapter the same way and pass the Yup schema to @validatedBody. (createZodValidator() / createYupValidator() are no-argument factories used when registering adapters on a ValidationRegistry manually.)
See also
- Validation: full reference for the validation system, registry, adapters, and error format.
- Configuration: typed environment variable validation with
defineConfig+Env.*. - Error handling: typed errors and the exception-filter pipeline. (Request validation failures are answered directly with a plain JSON 400; see the Validation error format.)