Playing Next Lesson In
seconds

Let's Learn AdonisJS 7 #3.3

Factories & Seeders

In This Lesson

Learn database seeders and factories in AdonisJS. Populate databases with preliminary data using factories and seeders.

Created by
@tomgobich
Published

If you'll recall from the last lesson, we hopped into a REPL session to make a role so we could create a user. It isn't ideal to have every teammate that pulls down our project do that manually. Instead it'd be great to have our application's roles created with a process similar to our migrations, that's where seeders come in.

Seeders allow us to populate our database with preliminary data. We might need preliminary data for a few different reasons, like:

  • Default data required by an application, like our roles

  • Development environments, to get teammates up and running as quickly as possible

  • Resetting a demo environment to showcase our project to prospective clients

We can use the Ace CLI's make:seeder command to easily add a seeder to our application. Let's make one called default we can use it for our roles.

node ace make:seeder default
# DONE:    create database/seeders/default_seeder.ts
Copied!

Seeders can be found within the database/seeders/ directory, and our new DefaultSeeder will look like below.

import { BaseSeeder } from '@adonisjs/lucid/seeders'

export default class extends BaseSeeder {
  async run() {
    // Write your database queries inside the run method
  }
}
Copied!

Exactly as the comment states, all we need to do is create our preliminary data within the run() method. That run() method will then be run when our seeder is called. So, for our roles, we can create a user and an admin role.

import { BaseSeeder } from '@adonisjs/lucid/seeders'
import Role from '#models/role'

export default class extends BaseSeeder {
  async run() {
    // Write your database queries inside the run method
    await Role.createMany([{ name: 'User' }, { name: 'Admin' }])
  }
}
Copied!
  • app
  • seeders
  • default_seeder.ts

In the vast majority of cases, you'll want to let your database set your records' IDs. However, for our roles, we have 1 hard-coded as the default role_id in our create_users_table migration. So, it would be great to force our role's ID in our seeder so we know for a fact we have a user role with an ID of 1 and an admin role with an ID of 2. We don't want to allow an ID of 1 to have a possibility of becoming a super user or anything of the sort.

import Role from '#models/role'
import { BaseSeeder } from '@adonisjs/lucid/seeders'

export default class extends BaseSeeder {
  async run() {
    // Write your database queries inside the run method
    await Role.createMany([
      { id: 1, name: 'User' },
      { id: 2, name: 'Admin' },
    ])
  }
}
Copied!
  • app
  • seeders
  • default_seeder.ts

We can take this a step further and give ourselves an enum-like const representation of these values to use throughout our application as well.

touch app/enums/roles.ts
Copied!
export const Roles = {
  USER: 1,
  ADMIN: 2,
} as const
Copied!
  • app
  • enums
  • roles.ts

Then, we can use that in place of hard-coded numbers so they're easier to read!

import Role from '#models/role'
import { BaseSeeder } from '@adonisjs/lucid/seeders'
import { Roles } from '../../app/enums/roles.ts'

export default class extends BaseSeeder {
  async run() {
    // Write your database queries inside the run method
    await Role.createMany([
      { id: Roles.USER, name: 'User' },
      { id: Roles.ADMIN, name: 'Admin' },
    ])
  }
}
Copied!
  • app
  • seeders
  • default_seeder.ts

Note that the import here is a relative import. If you prefer a subpath import, as our models use, we can add one to our package.json for our new enums directory.

// package.json
{
  ".": "...",
  "imports": {
    "#controllers/*": "./app/controllers/*.js",
    "#enums/*": "./app/enums/*.js",
    "#exceptions/*": "./app/exceptions/*.js",
    "#models/*": "./app/models/*.js",
    "#mails/*": "./app/mails/*.js",
    "#services/*": "./app/services/*.js",
    "#listeners/*": "./app/listeners/*.js",
    "#generated/*": "./.adonisjs/server/*.js",
    "#events/*": "./app/events/*.js",
    "#middleware/*": "./app/middleware/*.js",
    "#validators/*": "./app/validators/*.js",
    "#providers/*": "./providers/*.js",
    "#policies/*": "./app/policies/*.js",
    "#abilities/*": "./app/abilities/*.js",
    "#database/*": "./database/*.js",
    "#tests/*": "./tests/*.js",
    "#start/*": "./start/*.js",
    "#config/*": "./config/*.js"
  },
  ".": "...",
}
Copied!

We can then switch our role import to be from #enums/roles.

import Role from '#models/role'
import { BaseSeeder } from '@adonisjs/lucid/seeders'
import { Roles } from '#enums/roles'

export default class extends BaseSeeder {
  async run() {
    // Write your database queries inside the run method
    await Role.createMany([
      { id: Roles.USER, name: 'User' },
      { id: Roles.ADMIN, name: 'Admin' },
    ])
  }
}
Copied!
  • app
  • seeders
  • default_seeder.ts

Again, we can also update our migration; the value will remain the same, so no need to refresh or re-run the migration for this change.

import { BaseSchema } from '@adonisjs/lucid/schema'
import { Roles } from '#enums/roles'

export default class extends BaseSchema {
  protected tableName = 'users'

  async up() {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id').notNullable()
      table
        .integer('role_id')
        .unsigned()
        .references('id')
        .inTable('roles')
        .notNullable()
        .defaultTo(Roles.USER)
      table.string('full_name').nullable()
      table.string('email', 254).notNullable().unique()
      table.string('password').notNullable()

      table.timestamp('created_at').notNullable()
      table.timestamp('updated_at').nullable()
    })
  }

  async down() {
    this.schema.dropTable(this.tableName)
  }
}
Copied!
  • database
  • migrations
  • xxxxx_create_users_table.ts

Running Seeders

We can run seeders using the db:seed Ace CLI command. This will run all seeders defined in our application, though we can specify a specific file or files using the --files option or its alias -f. There's also an interactive mode we can use with --interactive or its alias -i. The interactive mode will allow us to select specifically which seeders we'd like to run from a list of our seeders.

node ace db:seed
Copied!

However, if we were to try to run our seeder at the moment, we would get a unique constraint.

node ace db:seed
#❯ error     database/seeders/default_seeder
#  insert into `roles` (`created_at`, `id`, `name`, `updated_at`) values ('2026-02-06 01:00:50', 1, 'User', '2026-02-06 01:00:50') - UNIQUE constraint failed: roles.id
Copied!

Remember, we've already created a role with an ID of 1 in the last lesson using REPL, so the unique constraint is complaining that the ID of 1 we're trying to insert already exists. So, let's wipe our database's data first!

node ace migration:fresh
Copied!

This will drop all our tables and rerun all our migrations. Then, we can retry our seeder.

node ace db:seed
# "sqlite" Role (2.27 ms) INSERT INTO `roles` (`created_at`, `id`, `name`, `updated_at`) VALUES (?, ?, ?, ?) [ '2026-02-06 01:07:31', 1, 'User', '2026-02-06 01:07:31' ]
# "sqlite" Role (56 μs) INSERT INTO `roles` (`created_at`, `id`, `name`, `updated_at`) VALUES (?, ?, ?, ?) [ '2026-02-06 01:07:31', 2, 'Admin', '2026-02-06 01:07:31' ]
# ❯ completed database/seeders/default_seeder
Copied!

By the way, we could've done all of that in a single command with:

node ace migration:fresh --seed
# or
node ace migration:refresh --seed
Copied!

This would've run migration:fresh or migration:refresh and all our seeders in a single command.

Environment Specific Seeders

Our default seeder, creating our roles, we want to run in any environment because every environment needs our roles to exist. However, if we're creating dummy data for our development environment, we don't want to run the risk of accidentally running that in production. To avoid that, we can gate it to specific environments.

To start, let's make a dev seeder.

node ace make:seeder dev
Copied!

Then, to gate it so it will only run in development, we can add a static environment property on the class. This accepts a string array of the environments we want this seeder to be allowed to run within. So, by adding "development" to this array, we're limiting the seeder so it can only run in our development environment.

import { BaseSeeder } from '@adonisjs/lucid/seeders'

export default class extends BaseSeeder {
  static environment: string[] = ['development']

  async run() {
    // Write your database queries inside the run method
  }
}
Copied!
  • database
  • seeders
  • dev_seeder.ts

Now, we don't want to have to create a bunch of dummy data for development by hand, as we did with our roles. To fix that, we can use factories.

Factories

Factories are wrappers around our models to aid in creating real database records with fake data. This can be handled in two scenarios.

  1. Quickly seeding development data without manual data entry.

  2. Aide with testing, allowing us to create data with breeze on a per-test basis.

We can easily create factories using the Ace CLI's make:factory command, or by providing the -f flag when using the make:model command. We've already created our models, so let's use make:factory, starting with our challenge.

node ace make:factory challenge
# DONE:    create database/factories/challenge_factory.ts
Copied!

We can find our factories within the database/factories/ directory, and our new RoleFactory will look like below.

import factory from '@adonisjs/lucid/factories'
import Challenge from '#models/challenge'

export const ChallengeFactory = factory
  .define(Challenge, async ({ faker }) => {
    return {}
  })
  .build()
Copied!
  • database
  • factories
  • challenge_factory.ts

Factories are defined by passing it our model's class, Challenge, as the first argument. Then, in the second argument, we use a callback function to return a representation of how that model's data should be filled with fake data. To do this, we'll use faker which is being destructured from the FactoryContext.

Despite our challenges table containing the following columns:

  • id

  • creator_id

  • text

  • points

  • created_at

  • updated_at

We only need to worry about specifying our our text and points columns should be populated with fake data. Our database will automatically handle id. AdonisJS will automatically handle our created_at and updated_at thanks to the options provided to the @column.dateTime() decorator on the column's model representation. Then, creator_id relates a challenge to a user and has a foreign key constraint. So, we'd be better off deferring that to a relationship, as we'll see in a bit.

Defining Factory Properties

So, to start, we can add a text property to the object being returned by our factory's callback method. Then, we can use faker to describe what sort of fake value should be used to populate text. Using the model passed into the first argument, Lucid will also provide us with type safety here and autocomplete for our model's properties.

import Challenge from '#models/challenge'
import factory from '@adonisjs/lucid/factories'

export const ChallengeFactory = factory
  .define(Challenge, async ({ faker }) => {
    return {
      text: faker.git.commitMessage(),
    }
  })
  .build()
Copied!
  • database
  • factories
  • challenge_factory.ts

The faker object contains several category-based properties, and each category contains another object with methods inside it. We'll call one of those methods to describe the type of value that should be used for our property. Within faker there isn't a great option that suits a "challenge", but we can use a like-value we should be familiar with by utilizing faker.git.commitMessage() to assign our text a fake commit message, like "reboot cross-platform driver."

Next, we can do the same for our points, describing it should be an integer number. Some of these faker methods accept options, like the int() method. We can use it to keep the fake value within a min and max range matching that of our validation, 1 and 100.

import Challenge from '#models/challenge'
import factory from '@adonisjs/lucid/factories'

export const ChallengeFactory = factory
  .define(Challenge, async ({ faker }) => {
    return {
      text: faker.git.commitMessage(),
      points: faker.number.int({ min: 1, max: 100 }),
    }
  })
  .build()
Copied!
  • database
  • factories
  • challenge_factory.ts

Fantastic! Before we can add relationships to our factory, we must first have a factory representation of the related model. So, let's go ahead and create our other two factories. Again, similar to our model the challenge_user table is a pivot table and can be sufficiently represented by a relationship between our Challenge and User model. Then, we won't be making a factory for our Role model because those are usually static within an application and won't need to use fake data.

node ace make:factory profile
# DONE:    create database/factories/profile_factory.ts
Copied!
node ace make:factory user
# DONE:    create database/factories/user_factory.ts
Copied!

For our ProfileFactory, we just need to specify how to populate our bio and is_public columns. Within the person category there is a one-to-one match for our bio. Then, in datatype there is a one-to-one match for a boolean value for our is_public flag.

import Profile from '#models/profile'
import factory from '@adonisjs/lucid/factories'

export const ProfileFactory = factory
  .define(Profile, async ({ faker }) => {
    return {
      bio: faker.person.bio(),
      isPublic: faker.datatype.boolean(),
    }
  })
  .build()
Copied!
  • database
  • factories
  • profile_factory.ts

Finally, we have our UserFactory. Like we did in our migration, we can hard-code the roleId to just 1 which will represent our basic "user" role. Then we have one-to-one matches for our fullName and email columns. For our password, we could use faker.internet.password(). However, our User model will hash the password before saving it into the database. Meaning, if we wanted to login as any of these fake users, we'd need to reset their password. Since we're in development and using a database on our local machine, we can save ourselves the hassle and hard-code the password to something so we know everyone's password on our local database.

import User from '#models/user'
import factory from '@adonisjs/lucid/factories'

export const UserFactory = factory
  .define(User, async ({ faker }) => {
    return {
      roleId: 1,
      fullName: faker.person.fullName(),
      email: faker.internet.email(),
      password: 'something',
    }
  })
  .build()
Copied!
  • database
  • factories
  • user_factory.ts

Factory Relationships

By adding relationships to our factories, we're giving ourselves the ability to create a fake challenge with a fake creator and a bunch of fake participants without having to specify any data. We can easily define a factory relationship using the relation() method. We'll pass the property name of the relationship as the first argument and give a callback function returning that relationship's factory as the second.

import User from '#models/user'
import factory from '@adonisjs/lucid/factories'
import { ChallengeFactory } from './challenge_factory.ts'
import { ProfileFactory } from './profile_factory.ts'

export const UserFactory = factory
  .define(User, async ({ faker }) => {
    return {
      roleId: 1,
      fullName: faker.person.fullName(),
      email: faker.internet.email(),
      password: faker.internet.password(),
    }
  })
  .relation('challenges', () => ChallengeFactory)
  .relation('createdChallenges', () => ChallengeFactory)
  .relation('profile', () => ProfileFactory)
  .build()
Copied!
  • database
  • factories
  • user_factory.ts

Let's go ahead and fill in the profile's relationship to the user.

import Profile from '#models/profile'
import factory from '@adonisjs/lucid/factories'
import { UserFactory } from './user_factory.ts'

export const ProfileFactory = factory
  .define(Profile, async ({ faker }) => {
    return {
      bio: faker.person.bio(),
      isPublic: faker.datatype.boolean(),
    }
  })
  .relation('user', () => UserFactory)
  .build()
Copied!
  • database
  • factories
  • profile_factory.ts

Finally, our challenge's relationship mirrors our user.

import Challenge from '#models/challenge'
import factory from '@adonisjs/lucid/factories'
import { UserFactory } from './user_factory.ts'

export const ChallengeFactory = factory
  .define(Challenge, async ({ faker }) => {
    return {
      text: faker.git.commitMessage(),
      points: faker.number.int({ min: 1, max: 100 }),
    }
  })
  .relation('createdBy', () => UserFactory)
  .relation('participants', () => UserFactory)
  .build()
Copied!
  • database
  • factories
  • challenge_factory.ts

Using Factories

Now that we have our factories filled out, we can use them to create dummy data for our development environment using our DevSeeder. Like our model, factories also have a create() method. However, since the instance is filled with our faker dummy data, we don't need to specify any data for it!

import { ChallengeFactory } from '#database/factories/challenge_factory'
import { BaseSeeder } from '@adonisjs/lucid/seeders'

export default class extends BaseSeeder {
  static environment: string[] = ['development']

  async run() {
    // Write your database queries inside the run method
    await ChallengeFactory.create()
  }
}
Copied!
  • database
  • seeders
  • dev_seeder.ts

One challenge isn't much for dummy data, though. Let's create a lot using createMany() instead! All we need to do is tell it how many challenges to create, and it'll do the rest. Let's make 50!

import { ChallengeFactory } from '#database/factories/challenge_factory'
import { BaseSeeder } from '@adonisjs/lucid/seeders'

export default class extends BaseSeeder {
  static environment: string[] = ['development']

  async run() {
    // Write your database queries inside the run method
    await ChallengeFactory.createMany(50)
  }
}
Copied!
  • database
  • seeders
  • dev_seeder.ts

Our challenges also need a creator, that's a not-nullable property. So, to include a creator with our challenge, we can use its createdBy relationship to include a fake user with each fake factory.

import { ChallengeFactory } from '#database/factories/challenge_factory'
import { BaseSeeder } from '@adonisjs/lucid/seeders'

export default class extends BaseSeeder {
  static environment: string[] = ['development']

  async run() {
    // Write your database queries inside the run method
    await ChallengeFactory.with('createdBy', 50).createMany(50)
  }
}
Copied!
  • database
  • seeders
  • dev_seeder.ts

Our creator could also use a profile, though. To include a nested relationship, we can include a third argument to get access to our UserFactory builder to include a profile for the user.

import { ChallengeFactory } from '#database/factories/challenge_factory'
import { BaseSeeder } from '@adonisjs/lucid/seeders'

export default class extends BaseSeeder {
  static environment: string[] = ['development']

  async run() {
    // Write your database queries inside the run method
    await ChallengeFactory
      .with('createdBy', 50, (builder) => builder.with('profile'))
      .createMany(50)
  }
}
Copied!
  • database
  • seeders
  • dev_seeder.ts

This will create 50 challenges with 50 users, one creator per challenge. What if we instead wanted a pool of 10 creators to randomly spread across our 50 challenges? Well, we could start by first creating our users.

import { ChallengeFactory } from '#database/factories/challenge_factory'
import { UserFactory } from '#database/factories/user_factory'
import { BaseSeeder } from '@adonisjs/lucid/seeders'

export default class extends BaseSeeder {
  static environment: string[] = ['development']

  async run() {
    // create 10 users and grab their ids
    const creators = await UserFactory.with('profile').createMany(10)
    const creatorIds = creators.map((user) => user.id)

    await ChallengeFactory.createMany(50)
  }
}
Copied!
  • database
  • seeders
  • dev_seeder.ts

Then, we can tap() into each challenge prior to it being persisted to randomly set which of our creatorIds get set as that challenge's creatorId.

import { ChallengeFactory } from '#database/factories/challenge_factory'
import { UserFactory } from '#database/factories/user_factory'
import { BaseSeeder } from '@adonisjs/lucid/seeders'

export default class extends BaseSeeder {
  static environment: string[] = ['development']

  async run() {
    // create 10 users and grab their ids
    const creators = await UserFactory.with('profile').createMany(10)
    const creatorIds = creators.map((user) => user.id)

    // create 50 challenges with a random creator
    await ChallengeFactory.tap((challenge, { faker }) => {
      challenge.creatorId = faker.helpers.arrayElement(creatorIds)
    }).createMany(50)
  }
}
Copied!
  • database
  • seeders
  • dev_seeder.ts

The tap() method is run once for each challenge being created. It receives an individual challenge model instance as its first parameter and the FactoryContext as its second parameter. So, we have faker at our disposal and faker contains a helper, arrayElement, that returns a random item from the provided array. In turn, this gets us a random user from our pool of created users.

Finally, we could use some participants. However, to do those from a pool of users as we did with our creators, we would need to be familiar with creating Lucid relationships. Let's not jump ahead and instead just create 6 participants per challenge.

import { ChallengeFactory } from '#database/factories/challenge_factory'
import { UserFactory } from '#database/factories/user_factory'
import { BaseSeeder } from '@adonisjs/lucid/seeders'

export default class extends BaseSeeder {
  static environment: string[] = ['development']

  async run() {
    // create 10 users and grab their ids
    const creators = await UserFactory.with('profile').createMany(10)
    const creatorIds = creators.map((user) => user.id)

    // create 50 challenges with a random creator
    await ChallengeFactory.tap((challenge, { faker }) => {
      challenge.creatorId = faker.helpers.arrayElement(creatorIds)
    })
      .with('participants', 6, (builder) => builder.with('profile'))
      .createMany(50)
  }
}
Copied!

We've already run our DefaultSeeder, so specify that we specifically want to run our DevSeeder via the db:seed Ace CLI command.

node ace db:seed --files=database/seeders/dev_seeder  
Copied!

A couple of things to note:

  1. The --files option should point to the seeder file from the application root

  2. Factories are created in a transaction, and we're creating many records with individual operations, so this may take a second

With that, if we boot our application back up with npm run dev and head back into our /challenges page, we should see 50 challenges, each with varying point values!

If you're interested in learning more about using factories, especially in tests, check out our series on testing with Japa!

Join the Discussion 0 comments

Create a free account to join in on the discussion
robot comment bubble

Be the first to comment!