Skip to main content

Data Modelling

OPRA's type system is the single source of truth for your API's data contract. You define your types once in TypeScript and OPRA uses those definitions to validate incoming requests, coerce values to their correct runtime types, filter output fields by scope, and generate the API schema — all automatically.


Type categories

TypeWhat it is
Simple typesScalars — string, number, date, and custom domain primitives with their own coercion and validation
Complex typesStructured objects with named fields (@ApiField). The primary tool for entities, request bodies, and response shapes
EnumsClosed sets of allowed values. Any value outside the set is rejected at decode time
Mapped typesProjections of an existing complex type — fields picked, omitted, or made optional without redeclaring them
MixinsMerges of multiple complex types into one class — the equivalent of multiple inheritance
ArraysTyped array wrappers around any other type, with minOccurs / maxOccurs constraints

A model in practice

A complex type is a plain TypeScript class. Fields are declared with @ApiField() and carry their full contract — type, required status, and constraints:

import { ComplexType, ApiField } from '@opra/common';

@ComplexType()
export class Customer {
@ApiField({ type: 'integer' })
declare id: number;

@ApiField({ required: true })
declare givenName: string;

@ApiField({ required: true })
declare familyName: string;

@ApiField()
declare email?: string;

@ApiField({ scope: 'admin' }) // never sent to non-admin callers
declare internalScore?: number;
}

From this single declaration OPRA derives:

  • Input validation — missing required fields, wrong types, and out-of-range values are rejected before the handler is called
  • Output encoding — response values are coerced to declared types; internalScore is stripped for public callers automatically
  • Schema — the type appears in GET /$schema and is used by oprimp to generate typed client code

Request body variants with mapped types

Rather than defining a separate DTO by hand, derive it from the entity:

import { OmitType, PartialType } from '@opra/common';
import { Customer } from './customer.js';

// Create DTO — server assigns id, clients don't submit it
export class CustomerCreate extends OmitType(Customer, ['id']) {}

// Patch DTO — all fields optional for partial update
export class CustomerPatch extends PartialType(Customer) {}

DATA MODELS — full reference for all type categories