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

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;
}

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 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.