Skip to content

Recipes

Practical, copy-pasteable patterns for real-world seeding. Each recipe sticks to the public API — combine them freely.

Prerequisites

These recipes assume you already have a factory and a seeder wired up. If not, start with Factories and Seeders, then come back here.

1. Idempotent seeders

Re-running npm run seed should not double your data. Skip seeding when the table already has rows — unless the run is a --refresh, in which case drop() already cleared everything first.

ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Seeder, SeederName, DataFactory } from '@ackplus/nest-seeder';
import { User } from '../entities/user.entity';
import { UserFactory } from '../factories/user.factory';

@Injectable()
@SeederName('users')
export class UserSeeder implements Seeder {
  constructor(
    @InjectRepository(User) private readonly userRepository: Repository<User>,
  ) {}

  async seed(): Promise<void> {
    const existing = await this.userRepository.count();
    if (existing > 0) {
      // Already seeded — nothing to do.
      return;
    }

    const factory = DataFactory.createForClass(UserFactory);
    await this.userRepository.save(factory.generate(10));
  }

  async drop(): Promise<void> {
    await this.userRepository.createQueryBuilder().delete().execute();
  }
}

How --refresh interacts with this

On nest-seed --refresh, every seeder's drop() runs first (in reverse order), so count() returns 0 and the guard lets the seed proceed. On a plain run, the guard makes the seeder a no-op once data exists.

2. Large datasets with batched inserts

Generating and inserting hundreds of thousands of rows in one save() can exhaust memory and blow past database parameter limits. Generate and persist in chunks instead.

ts
async seed(): Promise<void> {
  const factory = DataFactory.createForClass(UserFactory);

  const total = 100_000;
  const batchSize = 1_000;

  for (let inserted = 0; inserted < total; inserted += batchSize) {
    const count = Math.min(batchSize, total - inserted);
    const batch = factory.generate(count);
    await this.userRepository.save(batch);
  }
}

Mongoose variant

With Mongoose, use insertMany per batch for the same effect:

ts
await this.userModel.insertMany(factory.generate(count));

3. Relationships across seeders

Generate the parent rows first, then pass each parent's id as an override when generating children. Overrides may include keys that are not @Factory fields — like a foreign key.

ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Seeder, SeederName, DataFactory } from '@ackplus/nest-seeder';
import { User } from '../entities/user.entity';
import { Post } from '../entities/post.entity';
import { UserFactory } from '../factories/user.factory';
import { PostFactory } from '../factories/post.factory';

@Injectable()
@SeederName('posts')
export class PostSeeder implements Seeder {
  constructor(
    @InjectRepository(User) private readonly userRepository: Repository<User>,
    @InjectRepository(Post) private readonly postRepository: Repository<Post>,
  ) {}

  async seed(): Promise<void> {
    const userFactory = DataFactory.createForClass(UserFactory);
    const users = await this.userRepository.save(userFactory.generate(10));

    const postFactory = DataFactory.createForClass(PostFactory);
    for (const user of users) {
      // authorId is an override, not a @Factory field on PostFactory.
      const posts = postFactory.generate(3, { authorId: user.id });
      await this.postRepository.save(posts);
    }
  }

  async drop(): Promise<void> {
    await this.postRepository.createQueryBuilder().delete().execute();
  }
}

Order matters in the config

List parents before children in seeders: [...]. Seeders run top-to-bottom, and on --refresh they are dropped in reverse order — so children are dropped before parents, keeping foreign keys happy.

ts
seeders: [UserSeeder, PostSeeder] // users first; posts dropped first on refresh

4. Environment-specific seeders

Pick a different seeder list per NODE_ENV directly in your config file. The config is just code, so branch on the environment before exporting.

ts
// seeder.config.ts
import { TypeOrmModule } from '@nestjs/typeorm';
import { defineSeederConfig } from '@ackplus/nest-seeder';
import { User } from './src/database/entities/user.entity';
import { Post } from './src/database/entities/post.entity';
import { UserSeeder } from './src/database/seeders/user.seeder';
import { PostSeeder } from './src/database/seeders/post.seeder';
import { AdminSeeder } from './src/database/seeders/admin.seeder';

const isProd = process.env.NODE_ENV === 'production';

export default defineSeederConfig({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.DB_HOST ?? 'localhost',
      port: Number(process.env.DB_PORT ?? 5432),
      username: process.env.DB_USERNAME ?? 'postgres',
      password: process.env.DB_PASSWORD ?? 'postgres',
      database: process.env.DB_DATABASE ?? 'app',
      entities: [User, Post],
      synchronize: true,
    }),
    TypeOrmModule.forFeature([User, Post]),
  ],
  // In production seed only the essentials; locally seed demo data too.
  seeders: isProd
    ? [AdminSeeder]
    : [AdminSeeder, UserSeeder, PostSeeder],
});
bash
NODE_ENV=production npm run seed   # AdminSeeder only
npm run seed                       # full demo dataset

Need just one seeder for a quick run?

You don't have to change the config — use --name to target specific seeders:

bash
npm run seed -- --name admin

See the CLI guide for matching rules.

5. Passing data via --context

The --context flag accepts a JSON string that is forwarded to every seeder as options.context. Use it to parametrize a run without editing code.

bash
npm run seed -- --context '{"userCount": 50, "tenantId": "acme"}'

Read it inside seed() via the optional options argument:

ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
  Seeder,
  SeederName,
  DataFactory,
  SeederServiceOptions,
} from '@ackplus/nest-seeder';
import { User } from '../entities/user.entity';
import { UserFactory } from '../factories/user.factory';

@Injectable()
@SeederName('users')
export class UserSeeder implements Seeder {
  constructor(
    @InjectRepository(User) private readonly userRepository: Repository<User>,
  ) {}

  async seed(options?: SeederServiceOptions): Promise<void> {
    const count = options?.context?.userCount ?? 10;
    const tenantId = options?.context?.tenantId;

    const factory = DataFactory.createForClass(UserFactory);
    // tenantId rides along as an override (not a @Factory field).
    const users = factory.generate(count, { tenantId });
    await this.userRepository.save(users);
  }

  async drop(): Promise<void> {
    await this.userRepository.createQueryBuilder().delete().execute();
  }
}

INFO

options.context is Record<string, any>, so always guard with optional chaining and sensible defaults — a run without --context passes no context at all.

6. Deterministic data with faker.seed()

Seed faker's PRNG before generating to get the same data on every run — invaluable for reproducible fixtures and snapshot tests.

ts
import { faker } from '@faker-js/faker';
import { DataFactory } from '@ackplus/nest-seeder';
import { UserFactory } from '../factories/user.factory';

async seed(): Promise<void> {
  faker.seed(12345); // any fixed number → stable output

  const factory = DataFactory.createForClass(UserFactory);
  const users = factory.generate(10); // identical every time
  await this.userRepository.save(users);
}

Combine with --context

Let callers choose the seed so CI and local runs match:

ts
async seed(options?: SeederServiceOptions): Promise<void> {
  if (options?.context?.seed != null) {
    faker.seed(Number(options.context.seed));
  }
  // ...
}
bash
npm run seed -- --context '{"seed": 42}'

7. Custom providers injected into seeders

Seeders are regular NestJS providers, so they support constructor injection. Register any extra providers in the config's providers array, then inject them like you would anywhere else.

ts
// src/database/services/password.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class PasswordService {
  hash(plain: string): string {
    // ...your real hashing here
    return `hashed:${plain}`;
  }
}
ts
// src/database/seeders/user.seeder.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Seeder, SeederName, DataFactory } from '@ackplus/nest-seeder';
import { User } from '../entities/user.entity';
import { UserFactory } from '../factories/user.factory';
import { PasswordService } from '../services/password.service';

@Injectable()
@SeederName('users')
export class UserSeeder implements Seeder {
  constructor(
    @InjectRepository(User) private readonly userRepository: Repository<User>,
    private readonly passwordService: PasswordService,
  ) {}

  async seed(): Promise<void> {
    const factory = DataFactory.createForClass(UserFactory);
    const users = factory.generate(10).map((user) => ({
      ...user,
      password: this.passwordService.hash('changeme'),
    }));
    await this.userRepository.save(users);
  }

  async drop(): Promise<void> {
    await this.userRepository.createQueryBuilder().delete().execute();
  }
}

Register the provider so Nest can resolve it:

ts
// seeder.config.ts
import { defineSeederConfig } from '@ackplus/nest-seeder';
import { PasswordService } from './src/database/services/password.service';
import { UserSeeder } from './src/database/seeders/user.seeder';

export default defineSeederConfig({
  imports: [
    /* TypeOrmModule.forRoot(...), TypeOrmModule.forFeature([User]) */
  ],
  providers: [PasswordService],
  seeders: [UserSeeder],
});

8. Wrapping a seeder in a transaction (TypeORM)

For all-or-nothing seeding, inject the TypeORM DataSource and run your writes inside dataSource.transaction(). If anything throws, the whole batch rolls back.

ts
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { Seeder, SeederName, DataFactory } from '@ackplus/nest-seeder';
import { User } from '../entities/user.entity';
import { Post } from '../entities/post.entity';
import { UserFactory } from '../factories/user.factory';
import { PostFactory } from '../factories/post.factory';

@Injectable()
@SeederName('users')
export class UserSeeder implements Seeder {
  constructor(private readonly dataSource: DataSource) {}

  async seed(): Promise<void> {
    await this.dataSource.transaction(async (manager) => {
      const userFactory = DataFactory.createForClass(UserFactory);
      const users = await manager.save(User, userFactory.generate(10));

      const postFactory = DataFactory.createForClass(PostFactory);
      for (const user of users) {
        const posts = postFactory.generate(3, { authorId: user.id });
        await manager.save(Post, posts);
      }
    });
  }

  async drop(): Promise<void> {
    await this.dataSource.transaction(async (manager) => {
      await manager.createQueryBuilder().delete().from(Post).execute();
      await manager.createQueryBuilder().delete().from(User).execute();
    });
  }
}

DataSource is injectable out of the box

When you import TypeOrmModule.forRoot(...) in your seeder config, TypeORM registers DataSource in the DI container — no extra providers entry needed. Inside the callback, use the supplied manager (not your repositories) so every write joins the transaction.

See also

  • Factories@Factory, dependsOn, and context-aware generators
  • Seeders — the Seeder interface, @SeederName, and drop()
  • ConfigurationdefineSeederConfig, imports, providers
  • CLI--refresh, --name, --context, --dry-run, and more

Released under the MIT License.