Skip to main content

Model Mixins

Hysteria ORM provides composable function-based mixins that let you easily add common functionality to your models. Mixins are pre-built functions that return model classes with useful columns, behaviors, and patterns—without writing repetitive code.

What are Mixins?

Mixins are functions that return model classes containing common sets of columns and behaviors. Instead of manually adding the same columns (like createdAt, updatedAt, id) to every model, you can use a mixin to get all that functionality automatically.

Benefits:

  • DRY (Don't Repeat Yourself): Avoid duplicating common column definitions across models
  • Consistency: Ensure all models follow the same patterns for timestamps, IDs, etc.
  • Maintainability: Update common functionality in one place
  • Type Safety: Full TypeScript support with proper typing for instance properties and static methods
  • Composability: Combine multiple mixins together (e.g., timestamps + UUID primary key)

Available Mixins

timestampMixin

Adds automatic timestamp tracking with createdAt, updatedAt, and deletedAt columns.

import { timestampMixin, column } from 'hysteria-orm';

export class User extends timestampMixin() {
@column()
declare name: string;

@column()
declare email: string;

// createdAt, updatedAt, and deletedAt are inherited from timestampMixin
}

// TypeScript knows about the timestamp properties
const user = new User();
user.createdAt; // Date
user.updatedAt; // Date
user.deletedAt; // Date | null

Inherited Properties:

  • createdAt: Date - Automatically set when record is created (autoCreate: true)
  • updatedAt: Date - Automatically updated when record is modified (autoUpdate: true)
  • deletedAt: Date | null - Used for soft delete functionality

TypeScript Interface:

interface TimestampFields {
createdAt: Date;
updatedAt: Date;
deletedAt: Date | null;
}

uuidMixin

Provides a UUID primary key column that's automatically generated.

import { uuidMixin, column } from 'hysteria-orm';

export class Product extends uuidMixin() {
@column()
declare name: string;

@column.float()
declare price: number;

// id: string (UUID) is inherited from uuidMixin
}

const product = new Product();
product.id; // string (UUID)

Inherited Properties:

  • id: string - UUID primary key with automatic generation

TypeScript Interface:

interface UuidFields {
id: string;
}

ulidMixin

Provides a ULID (Universally Unique Lexicographically Sortable Identifier) primary key.

import { ulidMixin, column } from 'hysteria-orm';

export class Event extends ulidMixin() {
@column()
declare name: string;

@column.datetime()
declare occurredAt: Date;

// id: string (ULID) is inherited from ulidMixin
}

const event = new Event();
event.id; // string (ULID, e.g., "01ARZ3NDEKTSV4RRFFQ69G5FAV")

Inherited Properties:

  • id: string - ULID primary key with automatic generation

TypeScript Interface:

interface UlidFields {
id: string;
}

incrementMixin

Provides an auto-incrementing integer primary key.

import { incrementMixin, column } from 'hysteria-orm';

export class Category extends incrementMixin() {
@column()
declare name: string;

@column()
declare description: string;

// id: number (auto-incrementing) is inherited from incrementMixin
}

const category = new Category();
category.id; // number

Inherited Properties:

  • id: number - Auto-incrementing integer primary key

TypeScript Interface:

interface IncrementFields {
id: number;
}

bigIntMixin

Provides a bigint auto-incrementing primary key (useful for tables expected to have many rows).

import { bigIntMixin, column } from 'hysteria-orm';

export class LogEntry extends bigIntMixin() {
@column()
declare message: string;

@column()
declare level: string;

// id: number (bigint in database) is inherited from bigIntMixin
}

const log = new LogEntry();
log.id; // number

Inherited Properties:

  • id: number - BigInt auto-incrementing primary key

TypeScript Interface:

interface BigIntFields {
id: number;
}

Composing Mixins

One of the most powerful features of Hysteria's mixins is their composability. You can combine multiple mixins to get all the functionality you need:

Combining Primary Key + Timestamps

import { timestampMixin, uuidMixin, column } from 'hysteria-orm';

// Compose timestampMixin with uuidMixin
export class User extends timestampMixin(uuidMixin()) {
static table = 'users';

@column()
declare name: string;

@column()
declare email: string;
}

// TypeScript knows about ALL the properties
const user = new User();
user.id; // string (UUID)
user.createdAt; // Date
user.updatedAt; // Date
user.deletedAt; // Date | null

// Static methods from Model are also available
User.find('some-uuid');
User.query().where('email', 'test@example.com').one();

All Composition Patterns

import {
timestampMixin,
uuidMixin,
ulidMixin,
incrementMixin,
bigIntMixin,
} from 'hysteria-orm';

// UUID + Timestamps
class UserWithUuid extends timestampMixin(uuidMixin()) {}

// ULID + Timestamps
class EventWithUlid extends timestampMixin(ulidMixin()) {}

// Auto-increment + Timestamps
class PostWithIncrement extends timestampMixin(incrementMixin()) {}

// BigInt + Timestamps
class LogWithBigInt extends timestampMixin(bigIntMixin()) {}

// Order doesn't matter for types, but convention is outer mixin first
class AlternativeOrder extends uuidMixin(timestampMixin()) {}

Creating Custom Mixins with createMixin

The easiest way to create custom mixins is using the createMixin factory. It accepts column definitions using the same ColumnOptions type used by decorators:

import { createMixin, timestampMixin, uuidMixin, column } from 'hysteria-orm';

interface AuditFields {
createdBy: string | null;
updatedBy: string | null;
}

const auditMixin = createMixin<AuditFields>({
createdBy: { nullable: true },
updatedBy: { nullable: true },
});

// Use your custom mixin - composes with other mixins
class Document extends auditMixin(timestampMixin(uuidMixin())) {
static table = 'documents';

@column()
declare title: string;
}

// All properties are typed
const doc = new Document();
doc.id; // string (from uuidMixin)
doc.createdAt; // Date (from timestampMixin)
doc.createdBy; // string | null (from auditMixin)

createMixin with Column Types

You can specify any column type supported by the ORM:

import { createMixin } from 'hysteria-orm';

interface ProfileFields {
bio: string | null;
avatarUrl: string | null;
followersCount: number;
isVerified: boolean;
metadata: Record<string, unknown> | null;
lastLoginAt: Date | null;
}

const profileMixin = createMixin<ProfileFields>({
bio: { type: 'text', nullable: true },
avatarUrl: { type: 'varchar', length: 500, nullable: true },
followersCount: { type: 'integer', default: 0 },
isVerified: { type: 'boolean', default: false },
metadata: { type: 'json', nullable: true },
lastLoginAt: { type: 'datetime', nullable: true },
});

class User extends profileMixin(timestampMixin(uuidMixin())) {
static table = 'users';

@column()
declare email: string;
}

Manual Mixin Creation

For more control, you can create mixins manually with function overloads:

import { Model, column } from 'hysteria-orm';
import type { AnyConstructor, Constructor } from 'hysteria-orm';

interface AuditFields {
createdBy: string | null;
updatedBy: string | null;
}

export function auditMixin(): typeof Model & Constructor<AuditFields>;
export function auditMixin<TBase extends AnyConstructor>(
Base: TBase
): TBase & Constructor<AuditFields>;
export function auditMixin<TBase extends AnyConstructor>(
Base?: TBase
): TBase & Constructor<AuditFields> {
const BaseClass = Base ?? Model;

class AuditModel extends (BaseClass as AnyConstructor) {
declare createdBy: string | null;
declare updatedBy: string | null;

static {
Model.column("createdBy", { nullable: true });
Model.column("updatedBy", { nullable: true });
}
}

return AuditModel as TBase & Constructor<AuditFields>;
}

Usage Examples

Blog Post with UUID and Timestamps

import { timestampMixin, uuidMixin, column, belongsTo } from 'hysteria-orm';
import { User } from './User';

export class Post extends timestampMixin(uuidMixin()) {
static table = 'posts';

@column()
declare title: string;

@column()
declare content: string;

@column()
declare userId: string;

@belongsTo(() => User, 'userId')
declare user: User;

// Inherited: id, createdAt, updatedAt, deletedAt
}

E-commerce Product with ULID

import { timestampMixin, ulidMixin, column, hasMany } from 'hysteria-orm';
import { OrderItem } from './OrderItem';

export class Product extends timestampMixin(ulidMixin()) {
static table = 'products';

@column()
declare name: string;

@column()
declare description: string;

@column.float()
declare price: number;

@column.integer()
declare stock: number;

@hasMany(() => OrderItem, 'productId')
declare orderItems: OrderItem[];

// Inherited: id (ULID), createdAt, updatedAt, deletedAt
}

Simple Category with Auto-increment

import { incrementMixin, column, hasMany } from 'hysteria-orm';
import { Product } from './Product';

export class Category extends incrementMixin() {
static table = 'categories';

@column()
declare name: string;

@column()
declare slug: string;

@hasMany(() => Product, 'categoryId')
declare products: Product[];

// Inherited: id (auto-increment number)
}

Type Exports

All field interfaces and utilities are exported for use in your own code:

import {
// Mixin factory
createMixin,
type MixinColumns,

// Field interfaces
type TimestampFields,
type UuidFields,
type UlidFields,
type IncrementFields,
type BigIntFields,

// Type utilities for manual mixin creation
type Constructor,
type AnyConstructor,
} from 'hysteria-orm';

// Use in generic functions
function processTimestamped<T extends TimestampFields>(record: T) {
console.log(`Created at: ${record.createdAt}`);
console.log(`Updated at: ${record.updatedAt}`);
}

Best Practices

1. Choose the Right Primary Key Mixin

Select mixins based on your use case:

MixinUse When
uuidMixinDistributed systems, need globally unique IDs before insert
ulidMixinNeed sortable unique IDs (lexicographically ordered by time)
incrementMixinTraditional auto-incrementing integer IDs
bigIntMixinTables expected to exceed 2 billion rows

2. Always Compose with Timestamps

Most models benefit from timestamp tracking:

// ✅ Good: Primary key + timestamps
class User extends timestampMixin(uuidMixin()) {}

// ⚠️ Less common: Just timestamps (no primary key from mixin)
class UserActivity extends timestampMixin() {
@column.uuid({ primaryKey: true })
declare id: string;
}

3. Set Table Names Explicitly

When using mixins, always set the table name explicitly:

class User extends timestampMixin(uuidMixin()) {
static table = 'users'; // Explicit table name
}

4. Leverage TypeScript

The mixins are fully typed. Use TypeScript's type inference:

class User extends timestampMixin(uuidMixin()) {
static table = 'users';
}

// TypeScript knows these exist
User.find('uuid'); // ✅ Typed
User.query().many(); // ✅ Typed
new User().createdAt; // ✅ Date
new User().id; // ✅ string

OpenAPI Integration

All built-in mixins include proper OpenAPI schemas for automatic API documentation:

// The mixins automatically include OpenAPI metadata
class Product extends timestampMixin(uuidMixin()) {
static table = 'products';

@column({
openApi: { type: "string", maxLength: 100, required: true }
})
declare name: string;

// id, createdAt, updatedAt, deletedAt already have OpenAPI metadata
}

Migration Considerations

When using mixins, ensure your database migrations include the inherited columns:

import { Migration } from 'hysteria-orm';

export default class CreateUsersTable extends Migration {
async up(): Promise<void> {
await this.schema.createTable('users', (table) => {
// From uuidMixin
table.uuid('id').primary();

// Your columns
table.string('name').notNullable();
table.string('email').notNullable().unique();

// From timestampMixin
table.timestamp('created_at').defaultTo(this.fn.now());
table.timestamp('updated_at').defaultTo(this.fn.now());
table.timestamp('deleted_at').nullable();
});
}

async down(): Promise<void> {
await this.schema.dropTable('users');
}
}

See Also


Next: Model Hooks & Lifecycle