Validation

Validation

Generated runtime validators from TypeScript types — JSDoc tags and branded phantom types.

tsgonest generates companion files (.tsgonest.js / .tsgonest.d.ts) alongside your compiled TypeScript output. Each companion exports runtime validation functions derived entirely from your type definitions — no schemas to write, no decorators to maintain.

Companion files

Running tsgonest build analyzes your TypeScript types and emits one companion file per exported type:

dist/
  user.dto.js                          ← tsgo output
  user.dto.CreateUserDto.tsgonest.js   ← generated validator + serializer
  user.dto.CreateUserDto.tsgonest.d.ts ← generated type declarations

Each companion exports three functions:

FunctionReturnsOn failure
validate{ success, data?, errors? }Returns error details
assertValidated TThrows error
serializeJSON-safe object

validate — safe parsing

validate returns a result object and never throws. Use it when you need to handle errors programmatically.

usage-validate.ts
import { validateCreateUserDto } from './user.dto.CreateUserDto.tsgonest.js';

const result = validateCreateUserDto(input);

if (result.success) {
  // result.data is typed as CreateUserDto
  console.log(result.data.email);
} else {
  // result.errors is ValidationErrorDetail[]
  for (const err of result.errors) {
    console.log(`${err.path}: expected ${err.expected}, received ${err.received}`);
  }
}

The errors array contains objects with the following shape:

validation-error.ts
interface ValidationErrorDetail {
  path: string;     // e.g. "address.zip"
  expected: string; // what was expected
  received: string; // what was received
}

assert — throw on failure

assert returns the validated data directly or throws a TsgonestValidationError. Use it when you want exceptions to propagate.

usage-assert.ts
import { assertCreateUserDto } from './user.dto.CreateUserDto.tsgonest.js';

try {
  const user = assertCreateUserDto(input);
  // user is typed as CreateUserDto
} catch (err) {
  // err is TsgonestValidationError
  console.log(err.errors); // ValidationErrorDetail[]
}

Two approaches to constraints

tsgonest supports two complementary ways to define validation constraints on your types.

1. JSDoc tags — zero dependencies

Write constraints as JSDoc tags directly in your type comments. No imports, no packages, no runtime footprint.

user.dto.ts
interface CreateUserDto {
  /** @format email */
  email: string;

  /** @minLength 8 @maxLength 128 */
  password: string;

  /** @minimum 0 @maximum 150 */
  age: number;
}

2. Branded phantom types — @tsgonest/types

Install @tsgonest/types and use intersection types for a fully type-safe, IDE-friendly experience. The branded types are phantom — they erase at runtime and add zero cost.

npm install @tsgonest/types
user.dto.ts
import { Email, MinLength, MaxLength, Min, Max } from '@tsgonest/types';

interface CreateUserDto {
  email: string & Email;
  password: string & MinLength<8> & MaxLength<128>;
  age: number & Min<0> & Max<150>;
}

Both approaches produce identical runtime validators. You can even mix them in the same interface.

NestJS integration

tsgonest automatically injects validation into your controllers at compile time. When you build with tsgonest build, the controller rewriter adds assertTypeName() calls for @Body(), @Query(), @Param(), and @Headers() parameters — no pipes or runtime setup needed.

user.controller.ts
@Controller('users')
export class UserController {
  @Post()
  create(@Body() dto: CreateUserDto) {
    // dto is automatically validated at compile time via injected assertCreateUserDto()
    return this.userService.create(dto);
  }

  @Get()
  findAll(@Query() query: PaginationDto) {
    // query is validated and number/boolean fields are auto-coerced from strings
    return this.userService.findAll(query);
  }

  @Get(':id')
  findOne(@Param('id') id: number) {
    // id is auto-coerced from string to number with NaN checking
    return this.userService.findOne(id);
  }
}

On validation failure, a TsgonestValidationError is thrown with structured error details (400 Bad Request).

Factory decorators are not supported

tsgonest uses static analysis to detect NestJS decorators. If you create factory functions that return core decorators (e.g., a function that wraps @Body(), @Query(), @Get(), @Post(), etc.), tsgonest cannot introspect them and will skip validation/coercion injection for those parameters.

// This works — tsgonest detects @Body() directly
create(@Body() dto: CreateUserDto) { ... }

// This does NOT work — tsgonest cannot see through the factory
const MyBody = () => Body();
create(@MyBody() dto: CreateUserDto) { ... } // no validation injected

Import aliases (import { Body as NestBody }) are supported — tsgonest resolves the original symbol.

Multipart / form-data

For file upload endpoints using multipart/form-data, use @FormDataBody() with FormDataInterceptor from @tsgonest/runtime:

upload.controller.ts
import { Controller, Post, UseInterceptors } from '@nestjs/common';
import { FormDataBody, FormDataInterceptor } from '@tsgonest/runtime';

@Controller('uploads')
export class UploadController {
  @Post()
  @UseInterceptors(FormDataInterceptor)
  upload(@FormDataBody(() => imageMulter()) body: UploadDto) {
    // body.image is the parsed file, body.name is a text field
    return this.uploadService.process(body);
  }
}

@FormDataBody() accepts a multer factory function. The FormDataInterceptor runs multer to parse the multipart request and merges files into the request body. This is a drop-in replacement for Nestia's @TypedFormData.Body()tsgonest migrate converts it automatically.

Next steps

On this page