Skip to content

Validation Pipe ​

validatorPipe(opts?) is the main entry point in @atscript/moost-validator.

It checks a handler argument's runtime type. If that type is an Atscript annotated type, it runs the model's validator before your handler executes.

The Most Common Setup ​

Register it globally:

typescript
import { Moost } from 'moost'
import { validatorPipe } from '@atscript/moost-validator'

const app = new Moost()
app.applyGlobalPipes(validatorPipe())

That is the best default for most apps.

Validate Request Bodies ​

This is the most common use case:

typescript
import { Controller } from 'moost'
import { Body, Post } from '@moostjs/event-http'
import { CreateUserDto } from './create-user.dto.as'

@Controller('users')
export class UsersController {
  @Post()
  async create(@Body() dto: CreateUserDto) {
    // dto has already been validated
    return this.users.create(dto)
  }
}

If validation fails, the pipe throws ValidatorError. For HTTP apps, pair it with Error Handling.

Normalized Input ​

When the parameter's type declares @normalize.* normalizers, the pipe applies them first (with normalizeForType()), validates the normalized value and passes it to your handler:

atscript
export interface CreateUserDto {
    @normalize.trim
    @normalize.lowercase
    email: string.email

    @normalize.trim
    name: string.required
}
typescript
@Post()
async create(@Body() dto: CreateUserDto) {
  // a body of { email: ' Ann@Example.COM ', name: ' Ann ' } arrives as
  // { email: 'ann@example.com', name: 'Ann' }
}

This applies to every parameter source, request bodies included. When union members normalize a value differently, the pipe settles it with its own validator options (partial, unknownProps, …), so a partial update is normalized as the member it validates as. The request's original object is not changed: the handler gets a normalized copy (or the same object when nothing needed normalizing). Together with the Coercion Pipe, the order is coerce → normalize → validate.

Validate PATCH Payloads ​

For partial updates, pass Atscript validator options to the pipe:

typescript
import { Controller } from 'moost'
import { Patch, Body } from '@moostjs/event-http'
import { UseValidatorPipe } from '@atscript/moost-validator'
import { UpdateUserDto } from './update-user.dto.as'

@Controller('users')
export class UsersController {
  @Patch(':id')
  @UseValidatorPipe({ partial: true })
  async patch(@Body() dto: UpdateUserDto) {
    return this.users.patch(dto)
  }
}

Use partial: 'deep' when nested objects should also allow missing fields.

Strip Unknown Properties ​

This is useful for request bodies that may contain extra fields:

typescript
app.applyGlobalPipes(
  validatorPipe({
    unknownProps: 'strip',
  })
)

Options:

  • 'error' — reject unknown props
  • 'ignore' — keep them
  • 'strip' — remove them from the value

Apply It Per Controller Or Handler ​

Global registration is usually best, but the decorator form is useful when only part of the app uses Atscript DTOs.

Per Controller ​

typescript
import { Controller } from 'moost'
import { UseValidatorPipe } from '@atscript/moost-validator'

@UseValidatorPipe()
@Controller('users')
export class UsersController {}

Per Handler ​

typescript
import { Controller } from 'moost'
import { Body, Post } from '@moostjs/event-http'
import { UseValidatorPipe } from '@atscript/moost-validator'
import { CreateUserDto } from './create-user.dto.as'

@Controller('users')
export class UsersController {
  @Post()
  @UseValidatorPipe()
  async create(@Body() dto: CreateUserDto) {}
}

UseValidatorPipe(opts?) is sugar for @Pipe(validatorPipe(opts)).

Validate Params And Query Values Carefully ​

The pipe validates the value it receives. It does not coerce strings into numbers, booleans, or dates.

That means:

  • body payloads are usually a good fit because JSON parsing already gives you numbers, booleans, arrays, and objects
  • raw HTTP params and query values arrive as strings, so on their own only string-based Atscript types fit there

Good example:

atscript
export type EmailQuery = string.email
typescript
import { Controller } from 'moost'
import { Get, Query } from '@moostjs/event-http'
import { EmailQuery } from './queries.as'

@Controller('users')
export class UsersController {
  @Get('search')
  async search(@Query('email') email: EmailQuery) {
    return this.users.searchByEmail(email)
  }
}

If a param or query needs numeric or boolean validation, register the Coercion Pipe ahead of this one — it converts "42" to 42 before validation runs, so numeric .as aliases work directly as param types.

Optional Params ​

Parameters marked with Moost's @Optional() decorator are skipped by the pipe when their value is undefined or null. This matters for slots that are genuinely missing at the framework level — optional query strings, optional CLI flags — where the underlying Atscript type itself is not nullable.

typescript
import { Controller, Optional } from 'moost'
import { Get, Query } from '@moostjs/event-http'
import { WikiName } from './types.as'

@Controller('search')
export class SearchController {
  @Get()
  async search(@Query('wiki') @Optional() wiki?: WikiName) {
    // `wiki` is undefined when the query string is absent — no validation runs.
    // When present, `WikiName` constraints (minLength, pattern, ...) are enforced.
  }
}

The pipe still validates the value whenever one is provided, so ?wiki= (empty) or ?wiki=bad name will fail with ValidatorError as usual.

For request bodies, this is rarely needed — bodies are usually required at the framework level, and individual properties can be marked optional inside the Atscript interface itself.

Use Reusable Validated Primitive Types ​

This is one of the nicest patterns in Atscript + Moost.

Define a validated primitive once:

atscript
export type Email = string.email

Then use those types directly in handlers:

typescript
import { Controller } from 'moost'
import { Body, Post } from '@moostjs/event-http'
import { Email } from './types.as'

@Controller('newsletter')
export class NewsletterController {
  @Post('subscribe')
  async subscribe(@Body() email: Email) {
    return this.newsletter.subscribe(email)
  }
}

That is hard to model with class-validator, because its validation model is centered on decorated classes and their properties. Atscript validates the type itself, so standalone reusable primitives work naturally.

Options At A Glance ​

validatorPipe(opts?) accepts the same Partial<TValidatorOptions> object as Atscript's runtime validator.

Most useful options:

  • partial — allow missing properties (PATCH)
  • unknownProps — 'strip' | 'ignore' | 'error'
  • errorLimit — stop after N errors

Advanced options ​

These are available but rarely needed in HTTP request validation. Reach for them only when you understand the underlying validator behavior:

  • skipList — paths to skip during validation
  • replace — value-replacement hook used by validator plugins
  • plugins — custom validator plugin functions

If you already know the TypeScript validator API, the same options work here. See Validation Reference for the full low-level option details.

Beyond HTTP ​

The pipe is not tied to HTTP. It works anywhere Moost resolves handler arguments.

What is HTTP-specific is the built-in error transform, because that part returns HttpError(400).

Next Steps ​

Released under the MIT License.