Skip to content

Validation

Moost uses Atscript for request validation. Atscript extends TypeScript with annotations that produce runtime validators from your types — no class-validator, no duplicate schemas, one source of truth for types and validation rules.

Atscript documentation

For installation, compiler setup, annotation syntax, and all available options see the Atscript docs and the @atscript/moost-validator package reference.

Quick example

1. Define a DTO in Atscript

atscript
// create-user.as

export interface CreateUserDto {
  @expect.minLength 2
  @expect.maxLength 100
  name: string

  email: string.email

  @meta.sensitive
  @expect.minLength 8
  password: string

  role?: 'admin' | 'user'
}

Atscript compiles this to a regular TypeScript module that also carries runtime metadata — validators, labels, constraints — all inferred from the annotations.

2. Use it in a handler

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

@UseValidatorPipe()
@UseValidationErrorTransform()
@Controller('users')
export class UsersController {
  @Post()
  create(@Body() dto: CreateUserDto) {
    // dto is validated — name, email, password all satisfy the Atscript schema
    return { created: dto.name }
  }
}

Import the DTO as a value, not a type

import type { CreateUserDto } is erased at compile time, so the runtime annotated type never reaches the decorator metadata and validation is silently skipped. Always use a value import for .as types used in validated parameters.

That's it. When a request arrives with an invalid body, the validator pipe rejects it before your handler runs and returns a structured 400 response. message is the first failure as "<path>: <message>", and the full error list is under _body:

json
{
  "statusCode": 400,
  "message": "email: Must be a valid email",
  "error": "Bad Request",
  "_body": [
    { "path": "email", "message": "Must be a valid email" },
    { "path": "password", "message": "Length must be >= 8" }
  ]
}

Enabling validation globally

Instead of decorating each controller, register the pipe and interceptor once on the app:

ts
import { Moost } from 'moost'
import { MoostHttp } from '@moostjs/event-http'
import { validatorPipe, validationErrorTransform } from '@atscript/moost-validator'

const app = new Moost()
app.adapter(new MoostHttp())
app.applyGlobalPipes(validatorPipe())
app.applyGlobalInterceptors(validationErrorTransform())
// register controllers...
await app.init()

Now every handler parameter whose type was compiled by Atscript is validated automatically.

Validator options

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

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

Why Atscript

ApproachSchemasValidation rulesRuntime cost
class-validator + class-transformerDuplicate class + decoratorsImperative decoratorsReflection + transform
Zod / YupSeparate schema objectBuilder APISchema parse
AtscriptYour TypeScript types are the schemaAnnotations in the type fileCompiled validators

Atscript eliminates the gap between the type you write and the validation that runs. One .as file produces the TypeScript type, the JSON Schema (for Swagger), and the runtime validator.

Further reading

Released under the MIT License.