Skip to main content
Version: 4.0.0-preview

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 IValidationAdapter and 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

src/usecases/users/create-user.dto.ts
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:

src/app.ts
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:

src/usecases/users/users.controller.ts
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.)