Course outline

NestJS / Basic: modules and request contracts / Lesson 5

DTO validation, pipes and runtime values

Build a runtime input contract instead of assuming TypeScript annotations validate incoming values.

About 4 min readContent reviewed
On this page

Lesson goals

  • Separate parsing, transformation, validation and domain authorization.
  • Explain whitelist behavior and explicit unknown-field rejection.

Types disappear while request values remain

A DTO class can describe expected input, but a TypeScript property annotation alone does not validate JSON or turn query strings into numbers. Pipes run at the request argument boundary to transform or reject values. Parsing syntax, checking structure, applying domain limits and authorizing a transition establish different facts. Choose the validation approach and bind it in the actual application. Nest 12 also documents schema-based alternatives, but attaching schema metadata without the corresponding validation pipe is not enforcement. This lesson uses the established class/decorator approach as an explicitly selected example.

Review validation options as public behavior

The DTO fragment assumes class-validator decorators and a configured ValidationPipe. whitelist removes properties without relevant validation metadata, while forbidNonWhitelisted can reject them rather than silently strip them. transform enables transformation under the selected setup; it is not permission to apply arbitrary implicit coercion without tests. Class instances and reflection metadata matter, so interfaces or type-only imports cannot substitute for the required runtime class. The example is uncompiled and no validation package was installed. Test unknown, missing, null and wrong-type values separately in a real isolated adapter.

typescript Illustrative class-based validation contract
export class CreateCourseDto {
  @IsString()
  @Length(1,60)
  title!: string;
}
// Application configuration fragment:
app.useGlobalPipes(new ValidationPipe({
  whitelist:true,forbidNonWhitelisted:true,transform:true
}));

Decorator imports, runtime metadata and app are assumed. The options require the actual pipe registration and do not execute here.

Keep patch presence and projection explicit

For a patch DTO, omission can mean preserve, null can mean clear and an empty string can be a supplied invalid or valid value according to the contract. Avoid truthiness checks that merge these cases. Whitelisting is not authorization: an allowed state field may still be forbidden for this principal or workflow stage. Build a narrow domain input after validation rather than mass-assigning every property to an entity. Decide how normalization such as trimming and Unicode length should behave, and keep client expectations aligned with the canonical value the service returns.

Test a narrow runtime rule independently

The plain JavaScript validator below requires an object with a string title and rejects unknown keys. It trims before checking one to sixty code points. That measure differs from grapheme clusters and storage bytes, so it is an explicit lab convention. It does not emulate ValidationPipe or class-validator. The assertions show valid normalization and rejection before any domain write. Keeping such a domain rule independent makes it easy to test multilingual boundaries, but the actual Nest adapter still needs tests proving it supplies the intended runtime values and maps failures safely.

javascript Complete offline domain-input validator
function courseInput(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("INVALID_INPUT");
  if (Object.keys(value).some(k => k !== "title")) throw new Error("UNKNOWN_FIELD");
  if (typeof value.title !== "string") throw new Error("INVALID_TITLE");
  const title=value.title.trim();
  if ([...title].length < 1 || [...title].length > 60) throw new Error("INVALID_TITLE");
  return {title};
}
console.assert(courseInput({title:"  Modules  "}).title === "Modules");
let denied=false; try {courseInput({title:"X",owner:"other"});} catch {denied=true;}
console.assert(denied);

The title is normalized and the unknown owner is rejected. No pipe, decorator or Nest request is exercised.

Validate the adapter and the domain separately

Use parameter pipes for specific route values where that keeps intent clear, and apply global policies deliberately rather than assuming every application context inherits them. A testing module alone may not reproduce production bootstrap configuration. Verify strings, repeated query values, missing bodies and invalid numbers through the actual selected HTTP adapter. Check exact public error fields and no write on rejection. This course runs the local validator only; it neither compiles DTO decorators nor tests parser, transformation, schema-based validation or HTTP status behavior. Keep that narrow scope visible in the course and future implementation reports.

Common errors and fixes

A DTO annotation accepts invalid runtime input

Why it happens
Compile-time types were mistaken for an installed validation policy.
How to fix it
Register the chosen pipe and runtime DTO/schema correctly.
Prevent it next time
Test actual input through the composed adapter.

An unknown owner reaches persistence

Why it happens
Input was mass-assigned without a narrow validated contract.
How to fix it
Reject or strip under policy and project domain fields.
Prevent it next time
Assert unknown sensitive fields cannot trigger writes.

Null clears a field intended to remain unchanged

Why it happens
Omission and explicit null were merged.
How to fix it
Preserve member presence and document patch semantics.
Prevent it next time
Test omitted, null and concrete values separately.

Check your understanding

1. Does whitelist enforce whether a user may publish a course?

Show worked solution

No. It controls accepted structure under validation metadata. A recognized field can still represent a forbidden action for the caller or current workflow. Apply shared domain authorization after validated input and before the effect. Test both malformed input and a well-formed but unauthorized transition because they exercise different boundaries.

2. Why is a TypeScript interface unsuitable as the only validation DTO?

Show worked solution

It is erased at runtime and cannot provide the class metadata expected by the selected decorator-based validation flow. Use the actual class and proper imports, or deliberately choose and register a schema-based alternative. Verify the full pipe configuration instead of assuming a matching static shape executes validation.

Key takeaways

  • Runtime validation and business permission are separate decisions.
  • Transformation, unknown fields and patch presence belong in the public contract.

Official references