DEV Community

Cover image for What If the Model Was the Source of Truth
Artem M
Artem M

Posted on

What If the Model Was the Source of Truth

Recommended reading: The DTO problem — the class-validator pain points that motivate Atscript's approach.

The previous article showed how class-validator DTOs duplicate the same truth across TypeScript types, decorator stacks, and nested class hierarchies. Zod moves that truth into a schema DSL. Both work — but both still require a separate description of data that lives apart from your types.

Atscript takes a different path. Instead of bridging types and validation after the fact, it makes the model itself the single source — for types, validation, database schema, UI metadata, and anything else your stack needs.

This article is a quick tour of what Atscript is and why it exists.

One file, many outputs

single source of truth

Here is a user model in Atscript:

// user.as
export type Email = string.email
export type PositiveInt = number.int & number.positive

export interface User {
  @meta.label 'Full Name'
  @expect.minLength 2
  name: string

  email: Email

  @expect.min 13
  age: PositiveInt

  status: 'active' | 'inactive'
}
Enter fullscreen mode Exit fullscreen mode

From this single .as file, Atscript generates:

  • TypeScript types — fully typed interfaces you import like any other module
  • Runtime validation — constraints enforced automatically
  • JSON Schema — for API documentation or contract testing
  • Runtime metadata — labels, descriptions, and custom annotations accessible programmatically

Semantic types replace decorator noise

In class-validator, you write @IsEmail() on a string. In Zod, you write z.string().email(). In both cases, the fact that something is an email lives outside the type system — in a decorator or a method chain.

Atscript builds meaning into the type itself:

string.email     // validates email format
string.uuid      // validates UUID
string.date      // validates date strings
number.int       // integer only
number.positive  // non-negative (>= 0)
number.timestamp // millisecond timestamp
Enter fullscreen mode Exit fullscreen mode

These are semantic types — primitive extensions that carry validation constraints by definition. string.email is not a string with a decorator attached. It is a type that means "email" at every level: compile time, runtime validation, and metadata.

The Email and PositiveInt aliases from the example above are reusable domain types built from these primitives. You can use them anywhere — as interface properties, function parameters, or building blocks for larger models. The validation travels with the type.

Learn more about semantic types

Annotations attach metadata, not just validation rules

Most validation libraries stop at constraints. Atscript's annotation system goes further — it attaches any kind of metadata to your model:

export interface Product {
  @meta.label 'Product Name'
  @meta.description 'Display name shown to customers'
  @meta.example 'Wireless Headphones'
  @expect.minLength 1
  @expect.maxLength 200
  name: string

  @meta.label 'Price'
  @meta.sensitive
  cost: number.positive

  @ui.component 'select'
  @ui.placeholder 'Choose a category'
  category: 'electronics' | 'clothing' | 'food'
}
Enter fullscreen mode Exit fullscreen mode

@expect.* handles validation. @meta.* handles labels, descriptions, examples, sensitivity markers. @ui.* hints at form rendering. All on the same model, all accessible at runtime through a single metadata API.

Learn more about annotations

Nested structures just work

One of the sharpest pains with class-validator is nested objects. Each one needs its own class, @ValidateNested(), and @Type(() => ClassName) wiring. Forget the @Type() decorator and validation silently skips the nested object.

In Atscript, nesting is just... nesting:

export interface Address {
  street: string
  city: string
  @expect.pattern "^[0-9]{5}$"
  zip: string
}

export interface Order {
  email: string.email
  items: OrderItem[]
  shipping: Address
  billing?: Address
}
Enter fullscreen mode Exit fullscreen mode

No wiring. No class-transformer. Optional fields are optional because of ?. Arrays validate each item automatically.

Ad-hoc annotations: metadata without modifying the model

Sometimes you need different metadata for the same model in different contexts — a user model needs one set of labels for an admin panel and another for a public registration form. Atscript handles this with ad-hoc annotations:

import { User } from './user'

export annotate User as AdminUserView {
  @meta.label 'Administrator Name'
  @ui.order 1
  name

  @meta.label 'Admin Email'
  @ui.order 2
  email
}
Enter fullscreen mode Exit fullscreen mode

This creates a new annotated variant of User without touching the original definition. The type stays the same; the metadata changes. Multiple views, one model.

Learn more about ad-hoc annotations

Validation is built in, not bolted on

built-in validation

Every Atscript type comes with a .validator() method:

import { Order } from './order.as'

const validator = Order.validator()

if (validator.validate(data)) {
  // data is narrowed to Order type
  processOrder(data)
} else {
  console.log(validator.errors)
  // [{ path: 'shipping.zip', message: '...' }]
}
Enter fullscreen mode Exit fullscreen mode

Errors include full dot-path locations (shipping.zip, items.0.productId), so you know exactly what failed and where.

Need partial validation for updates? No extra classes required:

// Top-level partial
Order.validator({ partial: true }).validate(data)

// Deep partial — nested fields optional too
Order.validator({ partial: 'deep' }).validate(data)
Enter fullscreen mode Exit fullscreen mode

Learn more about validation

The plugin system makes it extensible

Atscript is not a monolithic tool. The core parses .as files into an AST. Everything else — TypeScript generation, database adapters, validation — is a plugin.

// atscript.config.mts
import ts from '@atscript/typescript'

export default defineConfig({
  plugins: [ts()]
})
Enter fullscreen mode Exit fullscreen mode

The TypeScript plugin ships first, but the architecture is language-agnostic. The same .as model could generate Python dataclasses, Go structs, or OpenAPI specs — each through its own plugin.

You can also extend the type system itself. Need a string.phoneUS semantic type for your project? Define it in the config:

export default defineConfig({
  primitives: {
    string: {
      extensions: {
        phoneUS: {
          type: 'string',
          documentation: 'US phone number',
          expect: {
            pattern: ['^\\d{10}$', '', 'Must be 10 digits']
          }
        }
      }
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

Now string.phoneUS is a first-class semantic type with built-in validation — usable in any .as file across your project.

Learn more about plugins

Database annotations live on the same model

Atscript is not an ORM. But it does let you describe database concerns on the same model that handles types and validation:

@db.table 'users'
export interface User {
  @meta.id
  @db.default.fn 'uuid'
  id: string

  @db.index.unique 'email_idx'
  email: string.email

  name: string

  @db.default.fn 'now'
  createdAt?: number.timestamp.created
}
Enter fullscreen mode Exit fullscreen mode

From this, database adapters (SQLite, MongoDB) can generate schemas, run migrations, and provide type-safe CRUD — all derived from the same model that already handles your TypeScript types and validation.

The database layer is still experimental and the API may change — but the point here is that Atscript's annotation system is open-ended enough to carry database metadata alongside everything else, without the model becoming a framework-specific artifact.

Learn more about database integration

Not a validator — a model language

model language

class-validator validates. Zod validates. Atscript describes.

The .as file is a data model definition — and validation, metadata, database schema, and whatever else your plugins need all fall out of that definition naturally.

Getting started

Atscript has a VSCode extension with syntax highlighting and diagnostics, and a CLI for compilation.

The quick start guide covers installation and first model in under five minutes.


This is the last article in the series. It started with a router benchmark, moved through request context, backend composables, multi-transport events, framework design, performance, and the DTO problem — and ended here, at the data model layer. If any of that caught your attention, the Atscript docs are the next step.

Top comments (0)