Mixins & Reuse
Mixins are reusable field groups that eliminate repetition across your models. Instead of copy-pasting id, timestamps, and audit fields into every model, spread a mixin with one line.
The problem
Every model needs an ID. Most need timestamps. Many need audit trail fields or soft delete. Without mixins, you'd write the same 3-6 lines in every model. With 50+ models, that's hundreds of duplicated lines — and if you change the ID strategy, you change it everywhere.
Built-in mixins
Zanith provides these mixins out of the box. Use them with the spread syntax:
| Mixin | What it adds | Fields created |
|---|---|---|
...m.id() | UUID primary key | id: uuid, primary, default(uuid()) |
...m.intId() | Auto-increment integer PK | id: int, primary, default(autoincrement()) |
...m.timestamps() | Created + updated timestamps | createdAt: datetime default(now()), updatedAt: datetime autoUpdate |
...m.auditable() | Who created/updated this record | createdById: uuid nullable, updatedById: uuid nullable |
...m.orgScoped() | Multi-tenant foreign key | organizationId: uuid with database index |
...m.softDelete() | Soft delete support | deletedAt: datetime nullable |
Using mixins
Spread mixins at the top of your fields, then add model-specific fields below. You can use any combination:
const Order = defineModel((m) => ({ name: 'Order', table: 'orders', fields: { ...m.id(), // → id (uuid pk) ...m.timestamps(), // → createdAt, updatedAt ...m.auditable(), // → createdById, updatedById ...m.orgScoped(), // → organizationId (indexed) ...m.softDelete(), // → deletedAt (nullable) // Model-specific fields: total: m.float(), status: m.enum(OrderStatus).default('PENDING'), customerId: m.uuid().index(), },}));Custom mixins
Since the builder is plain TypeScript, creating custom mixins is just writing a function that returns an object of field builders. This is where Zanith's TypeScript-first approach shines — no special syntax, just functions.
// Custom mixin: reusable address fieldsexport function addressFields(m) { return { street: m.string(), city: m.string(), state: m.string(), zip: m.string(), country: m.string().default('US'), };} // Use in any model:const Customer = defineModel((m) => ({ name: 'Customer', table: 'customers', fields: { ...m.id(), ...m.timestamps(), ...addressFields(m), // ← reused across models name: m.string(), email: m.string().unique(), },})); const Warehouse = defineModel((m) => ({ name: 'Warehouse', table: 'warehouses', fields: { ...m.id(), ...addressFields(m), // ← same fields, different model capacity: m.int(), },}));Conditional mixins
Because mixins are just functions, you can use conditions:
function tenantFields(m, { multiTenant = false }) { if (!multiTenant) return {}; return { ...m.orgScoped(), // only added when multi-tenant is enabled };} const Product = defineModel((m) => ({ name: 'Product', table: 'products', fields: { ...m.id(), ...tenantFields(m, { multiTenant: true }), name: m.string(), },}));