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
Software Engineer · Nestbolt
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
UserFactorythat produces users with realistic names, emails, and roles via Faker - "States" —
factory.use(UserFactory).state("admin").create()for variants - A
DatabaseSeederthat orders dependent factories (users before posts before comments) - A
pnpm seedcommand 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/fakerinstalled (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
| Pitfall | What goes wrong | Fix |
|---|---|---|
email collisions on re-seed | unique constraint blocks the second run | Truncate tables before seeding, or use faker.string.uuid() for emails |
| Hardcoded passwords in seeders | Production-leaking dev data is a real incident | Use a fixed but documented password ("changeme"), and never run the seeder in production |
Faker calls in definition() returning the same value | Same value across all rows | Make sure you're using the faker argument, not a frozen one |
| Seeders depending on each other but not ordered | "Posts have no authors" errors | Use the order field on Seeder and document the dependency |
repo.save([entity, entity, ...]) with no batch limit | Postgres parameter-limit errors at ~30k rows | Page large count() calls into batches of 500 |
| Foreign keys wired by entity reference | TypeORM serializes the whole nested graph | Set the FK column directly: override({ authorId: user.id }) |
| Test code instantiating factories manually | Drift from production schema | Use 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>withentitygetter anddefinition(faker)method- A fluent
FactoryBuilder<T>with.count(n),.state(name),.override(values),.make(),.create(), plussequence()and lifecycle callbacks - A
Seederinterface with ordered execution FactoryModule.forRoot({ factories, seeders })to register everything globallyFactoryService.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.
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.