Skip to content

Validation Pipe with Atscript

Moost’s validation story is now powered by Atscript. Every DTO generated by Atscript exposes a validator(opts) factory that @atscript/moost-validator uses at runtime.

By combining the Moost pipeline system with these runtime hooks you get type-safe validation without writing imperative checks or maintaining parallel schemas.

How it works

  1. Generate annotated DTOs with Atscript (*.as source files) and consume them from your application code.
  2. Register the validation pipe provided by @atscript/moost-validator. The pipe inspects every handler argument and, when the declared type was annotated by Atscript, it runs validator().validate(value).
  3. Optionally register the error interceptor to convert rich validation failures into 400 Bad Request responses.
ts
import { Moost } from 'moost'
import { MoostHttp, Body, Post } from '@moostjs/event-http'
import { validatorPipe, validationErrorTransform } from '@atscript/moost-validator'
import { UsersController } from './users.controller'

const app = new Moost()
app.adapter(new MoostHttp())
app.applyGlobalPipes(validatorPipe())
app.applyGlobalInterceptors(validationErrorTransform())
app.registerControllers(UsersController)
await app.init()

With this setup every request body, query param, or DI value whose type exposes validator() is validated before your handler executes.

Example DTO

ts
@label "Create User"
export interface CreateUserDto {
  @label "Display name"
  name: string

  @expect.pattern "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$", "u", "Invalid email"
  email: string

  @label "Password"
  password: string

  @labelOptional
  roles?: string[]
}

Atscript emits the metadata at build time. Check the Atscript reference for the full authoring experience, including decorators, CLI options, and IDE integration.

Applying the pipe locally

You can scope validation to a specific controller or method using the @UseValidatorPipe() decorator, or stack it with other pipes via @Pipe().

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

@UseValidatorPipe()
@UseValidationErrorTransform()
@Controller('users')
export class UsersController {
  @Post()
  async create(@Body() dto: CreateUserDto) {
    // dto is guaranteed to satisfy the Atscript schema
  }
}

Per-parameter configuration

validatorPipe(opts) accepts any subset of TValidatorOptions. For instance, to allow unknown properties and collect all errors:

ts
app.applyGlobalPipes(
  validatorPipe({
    unknwonProps: 'ignore',
    errorLimit: 50,
  }),
)

The options are passed straight to Atscript’s validator, so you get feature parity with the standalone runtime.

Handling validation errors

When validation fails validator.validate() throws ValidatorError. Without the interceptor Moost would treat the error as unhandled and return 500 Internal Server Error. The provided interceptor (validationErrorTransform() or @UseValidationErrorTransform()) converts it into HttpError(400) with the underlying details exposed in the response body:

json
{
  "statusCode": 400,
  "message": "Validation failed",
  "errors": [
    { "path": "email", "message": "Invalid email" },
    { "path": "password", "message": "Length must be >= 12" }
  ]
}

Adapters such as @moostjs/event-http serialise that object as JSON automatically.

Validating beyond HTTP

Because the pipe hooks into Moost’s generic pipeline system you can reuse it for:

  • CLI handlers in @moostjs/event-cli
  • Workflow steps in @moostjs/event-wf
  • Any custom event adapter you build

Wherever Moost can resolve parameters, validatorPipe() can run.

Released under the MIT License.