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:
| Mixin | Use When |
|---|---|
uuidMixin | Distributed systems, need globally unique IDs before insert |
ulidMixin | Need sortable unique IDs (lexicographically ordered by time) |
incrementMixin | Traditional auto-incrementing integer IDs |
bigIntMixin | Tables 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
- Model Basics - Learn about basic model definition and decorators
- Model Hooks - Understand lifecycle hooks and custom behaviors
- Model Views - Working with database views
- Standard Methods - Learn about CRUD operations on models
Next: Model Hooks & Lifecycle