Think of a model as a blueprint for your data. If you have a “users” table in your database, you create a “User” model to work with user data in your JavaScript code.
Real-world analogy: A model is like a form at the doctor’s office. The form has specific fields (name, age, symptoms), and each filled-out form represents one patient record. The model defines what fields exist, and each instance represents one record.
# Create a modelnpx ilana make:model User# Create a model with migrationnpx ilana make:model User --migration# Create a model with everything (migration, factory, seeder)npx ilana make:model User --all
class User extends Model { // Specify table name (defaults to lowercase plural of class name) static table = 'users'; // Primary key column (defaults to 'id') static primaryKey = 'id'; // Primary key type ('number' or 'string') static keyType = 'number'; // Whether primary key auto-increments (true for numbers, false for UUIDs) static incrementing = true; // Database connection to use (optional) static connection = 'mysql';}
ULIDs are 26-character sortable IDs — they’re URL-safe, lexicographically ordered, and include a millisecond timestamp prefix so they sort naturally by creation time:
class Order extends Model { static table = 'orders'; static keyType = 'ulid'; static incrementing = false;}const order = await Order.create({ total: 49.99 });console.log(order.id); // "01J3X7KQZB8YTPNMCHW4RSVFGE"
Control which fields can be set when creating or updating records:
class User extends Model { // Fields that CAN be mass-assigned fillable = ['name', 'email', 'password']; // OR fields that CANNOT be mass-assigned guarded = ['id', 'is_admin', 'created_at']; // OR disable all protection (not recommended) // static unguarded = true;}// This worksconst user = await User.create({ name: 'John', email: 'john@example.com', password: 'secret',});// This would be ignored (not in fillable)const user = await User.create({ name: 'John', is_admin: true, // Ignored!});
Security Note: Always use fillable or guarded to prevent users from setting sensitive fields like is_admin or balance.
class User extends Model { hidden = ['password', 'remember_token', 'api_key'];}// Usageconst user = await User.find(1);console.log(user.toJSON()); // password field won't be included// Temporarily show hidden fieldsconsole.log(user.makeVisible(['password']).toJSON());
Mutators are called automatically whenever an attribute is set — on direct assignment (user.email = x), via fill(), and via update(). They must return the transformed value.
class User extends Model { // Called automatically any time password is assigned setPasswordAttribute(value) { return value ? bcrypt.hashSync(value, 10) : value; } // Called automatically any time email is assigned setEmailAttribute(value) { return value ? value.toLowerCase().trim() : value; } setNameAttribute(value) { return value ? value.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() ) : value; }}// Mutators fire automatically — no special call neededconst user = await User.create({ name: 'john doe', // Stored as "John Doe" email: ' JOHN@EXAMPLE.COM ', // Stored as "john@example.com" password: 'secret123', // Stored hashed});user.email = 'NEW@EXAMPLE.COM'; // Mutator fires on assignment tooconsole.log(user.email); // "new@example.com"
Accessors are called automatically on direct property access. List the key in appends to include it in toJSON() output.
class User extends Model { // Keys in appends are accessible as direct properties and included in toJSON() appends = ['full_name', 'avatar_url', 'formatted_created_at']; getFullNameAttribute() { return `${this.first_name} ${this.last_name}`; } getAvatarUrlAttribute() { return this.avatar ? `/uploads/avatars/${this.avatar}` : '/images/default-avatar.png'; } getFormattedCreatedAtAttribute() { return this.created_at.toLocaleDateString(); }}const user = await User.find(1);console.log(user.full_name); // "John Doe" — accessor called directlyconsole.log(user.avatar_url); // "/uploads/avatars/john.jpg"console.log(user.formatted_created_at); // "12/1/2023"// Also included in toJSON() because they're in appendsconst userData = user.toJSON();console.log(userData.full_name); // "John Doe"// Dynamically add an accessor key to appends at runtimeuser.append('full_name');
Sometimes you need to save records without triggering events — for example in seeders, migrations, or bulk imports. Use withoutEvents():
await User.withoutEvents(async () => { await User.create({ name: 'Admin', email: 'admin@example.com' }); await User.create({ name: 'Bot', email: 'bot@example.com' }); // creating/created/saving/saved events are all suppressed});// Events fire normally again after this point
// Find by primary keyconst user = await User.find(1);const user = await User.findOrFail(1); // Throws error if not found// Find by other attributesconst user = await User.findBy('email', 'john@example.com');// Get first recordconst user = await User.first();const user = await User.firstOrFail();// Get all recordsconst users = await User.all();// Find or createconst user = await User.firstOrCreate( { email: 'john@example.com' }, // Search criteria { name: 'John Doe' } // Additional data if creating);// Update or createconst user = await User.updateOrCreate( { email: 'john@example.com' }, // Search criteria { name: 'John Smith', is_active: true } // Data to update/create);
Pruning lets you automatically delete stale records (e.g. old logs, expired tokens) by defining what “prunable” means on the model:
class ActivityLog extends Model { static table = 'activity_logs'; // Return a query for records that should be deleted static prunable() { return this.query().where('created_at', '<', new Date(Date.now() - 90 * 86400_000)); // deletes logs older than 90 days }}// Delete all prunable records — processes in chunks of 1000 to avoid memory issuesconst deleted = await ActivityLog.prune();console.log(`Pruned ${deleted} records`);
Run prune() on a schedule (e.g. a daily cron job) to keep your tables clean.
// Re-fetch a fresh copy from the databaseconst user = await User.find(1);const freshUser = await user.fresh(); // new instance with up-to-date attributes// Check if two variables refer to the same database recordconst a = await User.find(1);const b = await User.find(1);console.log(a.is(b)); // true — same class + same primary keyconsole.log(a.isNot(b)); // falseconst c = await User.find(2);console.log(a.is(c)); // falseconsole.log(a.isNot(c)); // true
replicate() creates an unsaved copy of a model, excluding the primary key and timestamps so it can be inserted as a new row:
const original = await Post.find(1);const copy = original.replicate();// copy has same body/title/status — but no id or timestampscopy.title = 'Copy of ' + original.title;await copy.save(); // inserts as a new record
// Delete every row in the table (useful in tests / seeders)await User.truncate();// Create records using the model's registered factoryawait User.seed(10); // insert 10 users via factory
seed() requires a factory to be defined for the model with defineFactory. See Factories for setup details.
const user = await User.find(1);// Check if model exists in databaseconsole.log(user.exists); // true// Check if model has been modifieduser.name = 'New Name';console.log(user.isDirty()); // trueconsole.log(user.isDirty('name')); // trueconsole.log(user.isDirty('email')); // false// Get changed attributesconsole.log(user.getDirty()); // { name: 'New Name' }// Get original valuesconsole.log(user.getOriginal()); // Original data from databaseconsole.log(user.getOriginal('name')); // Original name value
// ✅ Goodclass User extends Model { fillable = ['name', 'email', 'password'];}// ❌ Dangerousclass User extends Model { static unguarded = true; // Anyone can set any field!}