Back to blog
factory·8 min read

How to Seed a NestJS Database (Factories, Fixtures, and Realistic Test Data)

A complete walkthrough of database seeding in NestJS — define factories with Faker, organize seeders, generate realistic test data, and stop writing the same insert scripts every project.

Khatab Wedaa

Khatab Wedaa

Software Engineer · Nestbolt

How to Seed a NestJS Database (Factories, Fixtures, and Realistic Test Data)

Every NestJS project hits the same moment: the schema is in place, the API endpoints work, and you need data. A few users to log in as. A handful of products to populate the catalogue. An admin account that exists in every developer's local database. The first instinct is to write a seed.ts script with a hundred lines of repo.save({ name: "Alice", email: "alice@example.com" }). It works. Two months later there are five copies of that script in the repo, half of them out of sync with the schema, and onboarding a new developer means running them in a specific order that's documented in someone's head.

This guide walks through doing it properly: defining factories that produce realistic data with Faker, organizing seeders that compose, and exposing a single command that fills a fresh database in seconds. The patterns work on TypeORM and translate directly to Prisma's seeding workflow. Along the way we'll cover the design choices that turn a one-off script into a reusable test-data layer.

What you'll build

By the end of this tutorial, your NestJS app will support:

  • A UserFactory that produces users with realistic names, emails, and roles via Faker
  • "States" — factory.use(UserFactory).state("admin").create() for variants
  • A DatabaseSeeder that orders dependent factories (users before posts before comments)
  • A pnpm seed command that drops you a populated dev database in under a second
  • Reusable factories in your tests (factory.use(UserFactory).count(50).make() for in-memory entities)

Prerequisites

  • Node.js 18 or later
  • A NestJS 10+ project with TypeORM configured
  • @faker-js/faker installed (npm install --save-dev @faker-js/faker)

1. Define your entities

Standard TypeORM entities — nothing factory-specific. Here's the small schema we'll seed:

// src/users/user.entity.ts
@Entity("users")
export class User {
  @PrimaryGeneratedColumn("uuid")
  id!: string;
 
  @Column()
  name!: string;
 
  @Column({ unique: true })
  email!: string;
 
  @Column({ default: "user" })
  role!: string;
}
// src/posts/post.entity.ts
@Entity("posts")
export class Post {
  @PrimaryGeneratedColumn("uuid")
  id!: string;
 
  @Column()
  title!: string;
 
  @Column({ type: "text" })
  body!: string;
 
  @ManyToOne(() => User, { eager: false })
  @JoinColumn({ name: "author_id" })
  author!: User;
 
  @Column({ name: "author_id" })
  authorId!: string;
}

2. Write a base factory

A factory is a class that knows how to produce one realistic instance of an entity. The base class handles persistence, batching, and overrides — subclasses just describe the shape of a record:

// src/factory/base.factory.ts
import { faker as defaultFaker } from "@faker-js/faker";
import type { Faker } from "@faker-js/faker";
import type { DataSource, Repository } from "typeorm";
 
export abstract class BaseFactory<T> {
  abstract get entity(): new () => T;
  abstract definition(faker: Faker): Partial<T>;
 
  protected faker: Faker = defaultFaker;
  private states: Array<(faker: Faker) => Partial<T>> = [];
  private overrides: Partial<T> = {};
  private quantity = 1;
 
  constructor(protected readonly dataSource: DataSource) {}
 
  count(n: number) {
    this.quantity = n;
    return this;
  }
 
  state(name: string) {
    const fn = (this as unknown as Record<string, (faker: Faker) => Partial<T>>)[name];
    if (typeof fn !== "function") {
      throw new Error(`State method "${name}" not found on ${this.constructor.name}`);
    }
    this.states.push(fn.bind(this));
    return this;
  }
 
  override(values: Partial<T>) {
    this.overrides = { ...this.overrides, ...values };
    return this;
  }
 
  private build(): T {
    const base = this.definition(this.faker);
    const stateAttrs = this.states.reduce(
      (acc, fn) => ({ ...acc, ...fn(this.faker) }),
      {} as Partial<T>,
    );
    const Entity = this.entity;
    return Object.assign(new Entity(), base, stateAttrs, this.overrides);
  }
 
  async make(): Promise<T[]> {
    return Array.from({ length: this.quantity }, () => this.build());
  }
 
  async create(): Promise<T[]> {
    const repo = this.dataSource.getRepository<T>(this.entity);
    const built = await this.make();
    return repo.save(built as Repository<T> extends Repository<infer X> ? X[] : never);
  }
}

The two methods that matter:

  • make() returns instances without persisting. Useful in tests where you want to exercise validation without hitting the database.
  • create() returns instances and saves them. The default for seeders.

3. Define a UserFactory

Subclass and implement definition(). State methods like admin() are just regular methods that return a partial — they get applied on top of the base definition():

// src/factory/user.factory.ts
import { BaseFactory } from "./base.factory";
import { User } from "../users/user.entity";
import type { Faker } from "@faker-js/faker";
 
export class UserFactory extends BaseFactory<User> {
  get entity() {
    return User;
  }
 
  definition(faker: Faker): Partial<User> {
    return {
      name: faker.person.fullName(),
      email: faker.internet.email().toLowerCase(),
      role: "user",
    };
  }
 
  admin(): Partial<User> {
    return { role: "admin" };
  }
 
  withDomain(domain: string): Partial<User> {
    return { email: this.faker.internet.email().toLowerCase().replace(/@.*$/, `@${domain}`) };
  }
}

Usage from a service:

// 10 regular users
await this.userFactory.count(10).create();
 
// 2 admins
await this.userFactory.count(2).state("admin").create();
 
// 5 users with a fixed company domain
await this.userFactory.count(5).override({ name: "Acme employee" }).create();
 
// In a test — no DB write
const previewUser = (await this.userFactory.make())[0];

The state-method pattern keeps the API readable. factory.state("admin") is self-explanatory; a chain of .override({ role: "admin", isVerified: true, mfaEnrolled: true }) calls is not.

4. Wire factories with a registry

A FactoryService registers factories at boot and returns the right one when you ask for it:

// src/factory/factory.service.ts
@Injectable()
export class FactoryService {
  private readonly factories = new Map<unknown, BaseFactory<unknown>>();
 
  constructor(
    @Inject(FACTORY_CLASSES) classes: Array<new (ds: DataSource) => BaseFactory<unknown>>,
    private readonly dataSource: DataSource,
  ) {
    for (const Cls of classes) {
      this.factories.set(Cls, new Cls(dataSource));
    }
  }
 
  use<T>(Cls: new (ds: DataSource) => BaseFactory<T>): BaseFactory<T> {
    const f = this.factories.get(Cls);
    if (!f) throw new Error(`Factory ${Cls.name} not registered`);
    return f as BaseFactory<T>;
  }
}

The matching module:

@Module({})
export class FactoryModule {
  static forRoot(opts: {
    factories: Array<new (ds: DataSource) => BaseFactory<unknown>>;
    seeders?: Array<new () => Seeder>;
  }): DynamicModule {
    return {
      module: FactoryModule,
      providers: [
        { provide: FACTORY_CLASSES, useValue: opts.factories },
        { provide: SEEDER_CLASSES, useValue: opts.seeders ?? [] },
        FactoryService,
      ],
      exports: [FactoryService],
      global: true,
    };
  }
}

Register in AppModule:

@Module({
  imports: [
    TypeOrmModule.forRoot({ /* ... */ }),
    FactoryModule.forRoot({
      factories: [UserFactory, PostFactory],
      seeders: [DatabaseSeeder],
    }),
  ],
})
export class AppModule {}

5. Compose seeders for ordered data

Once you have more than one factory, ordering matters. Posts need authors. Comments need posts. A Seeder interface lets you organize a top-level run:

// src/factory/seeder.interface.ts
import type { FactoryService } from "./factory.service";
 
export interface Seeder {
  order: number; // lower runs first
  run(factory: FactoryService): Promise<void>;
}
// src/seeders/database.seeder.ts
import type { Seeder } from "../factory/seeder.interface";
import { FactoryService } from "../factory/factory.service";
import { UserFactory } from "../factory/user.factory";
import { PostFactory } from "../factory/post.factory";
 
export class DatabaseSeeder implements Seeder {
  order = 0;
 
  async run(factory: FactoryService) {
    const admins = await factory.use(UserFactory).count(2).state("admin").create();
    const users = await factory.use(UserFactory).count(20).create();
 
    for (const user of [...admins, ...users]) {
      await factory.use(PostFactory).count(3).override({ authorId: user.id }).create();
    }
  }
}

The runner — also part of FactoryService:

async seed(): Promise<void> {
  const seeders: Seeder[] = (this.seederClasses ?? []).map((C) => new C());
  seeders.sort((a, b) => a.order - b.order);
  for (const s of seeders) await s.run(this);
}

6. Run it from a CLI command

The last piece is making pnpm seed work. NestJS apps spin up cleanly via NestFactory.createApplicationContext (no HTTP server, no listening port), which is the right shape for a CLI:

// src/cli/seed.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "../app.module";
import { FactoryService } from "../factory/factory.service";
 
async function bootstrap() {
  const app = await NestFactory.createApplicationContext(AppModule, {
    logger: ["error", "warn", "log"],
  });
  try {
    await app.get(FactoryService).seed();
    console.log("Seeded.");
  } finally {
    await app.close();
  }
}
 
bootstrap().catch((err) => {
  console.error(err);
  process.exit(1);
});

Wire it into package.json:

{
  "scripts": {
    "seed": "ts-node src/cli/seed.ts"
  }
}

A new developer's onboarding is now git clone, pnpm install, pnpm typeorm:migrate, pnpm seed — three minutes from clone to a working app with realistic data.

7. Common pitfalls

PitfallWhat goes wrongFix
email collisions on re-seedunique constraint blocks the second runTruncate tables before seeding, or use faker.string.uuid() for emails
Hardcoded passwords in seedersProduction-leaking dev data is a real incidentUse a fixed but documented password ("changeme"), and never run the seeder in production
Faker calls in definition() returning the same valueSame value across all rowsMake sure you're using the faker argument, not a frozen one
Seeders depending on each other but not ordered"Posts have no authors" errorsUse the order field on Seeder and document the dependency
repo.save([entity, entity, ...]) with no batch limitPostgres parameter-limit errors at ~30k rowsPage large count() calls into batches of 500
Foreign keys wired by entity referenceTypeORM serializes the whole nested graphSet the FK column directly: override({ authorId: user.id })
Test code instantiating factories manuallyDrift from production schemaUse the same FactoryService in tests via Test.createTestingModule

The "test code instantiating manually" one bites every team. The whole point of factories is that there's one place that knows how to make a valid User. If your tests new-up User objects directly, schema changes break tests but seeders pass — or vice versa. Use the factory everywhere.

Going further: @nestbolt/factory

Factories, registries, builders, seeders, and CLI wiring are each small. Together they're a non-trivial amount of boilerplate, and every project re-derives it slightly differently.

@nestbolt/factory is the version we extracted after re-writing this for the fourth time:

  • BaseFactory<T> with entity getter and definition(faker) method
  • A fluent FactoryBuilder<T> with .count(n), .state(name), .override(values), .make(), .create(), plus sequence() and lifecycle callbacks
  • A Seeder interface with ordered execution
  • FactoryModule.forRoot({ factories, seeders }) to register everything globally
  • FactoryService.use(UserFactory) for dependency-injected access from services or tests
  • Faker seeding for deterministic test data when you need it

Setup is the two-step pattern from this guide — FactoryModule.forRoot() with your factory classes, then factoryService.use(UserFactory).count(10).create() anywhere. The full API is in the factory quick-start.

Wrapping up

A good seeder is the difference between "spend a Tuesday wrestling with onboarding" and "the new developer has a working app twenty minutes after cloning." Factories give you composable, realistic data; seeders organize the ordering; the CLI makes it a single command. Once it's in place, every feature you ship gets a few lines of factory code — and your tests, your local development, and your staging environment all draw from the same definitions.

If this saved you from writing the same seed.ts for the third project, star the repo. If you want a follow-up on factory relations, sequences, or deterministic Faker seeds for snapshot tests, open an issue on GitHub.

Khatab Wedaa

Written by Khatab Wedaa

Software Engineer · Nestbolt

Building open-source NestJS packages — authentication, permissions, audit logs, media uploads, and the patterns every backend ends up rebuilding.