NestJS course Β· Module 3: TypeORM and Databases
Seeders and Fixtures - the cohort's starting equipment
In this lesson7
Consul Caesar.js is tired of a certain ritual. Every time a new developer joins the treasury project, or a fresh database is set up for tests, somebody types in the same data by hand: a few centurions, a couple of legionaries, the basic skills. A fresh database is an empty castra - and a cohort without equipment will not march on campaign. Time to hire a quartermaster who does this for us: a seeder.
Seeder vs fixtures - the code and the supplies
These two terms love to get mixed up, so let's separate them right away. A seeder is code - a class which, when run, fills the database with starting data. Fixtures are the data itself - static sets stored in files (JSON, YAML), waiting for someone to load them. In Roman terms: fixtures are the supply list on parchment, the seeder is the quartermaster who loads the wagons according to that list. A seeder can have its data written directly in code, or read it precisely from a fixtures file.
Your first seeder
We will use the typeorm-extension package, which adds seeding support to TypeORM. A seeder is a class implementing the Seeder interface with a single run method - it receives the database connection (dataSource), from which we take the repositories we already know:
1// seeds/centurion.seeder.ts
2import { DataSource } from 'typeorm';
3import { Seeder } from 'typeorm-extension';
4import { Centurion } from '../src/centurion/centurion.entity';
5
6export default class CenturionSeeder implements Seeder {
7 public async run(dataSource: DataSource): Promise<void> {
8 const centurionRepository = dataSource.getRepository(Centurion);
9
10 await centurionRepository.save([
11 { name: 'Maximus Decimus', cohortName: 'Legio X Equestris', yearsOfExperience: 15 },
12 { name: 'Livia Drusilla', cohortName: 'Legio III Gallica', yearsOfExperience: 8 },
13 ]);
14 }
15}Nothing beyond what you already know: we take a repository and call save with an array of objects. All the novelty is in the packaging: a class with a run method can be executed with a single command - on a fresh database, on every developer's machine, in every environment - and it will load exactly the same equipment every time.
Cleaning and idempotency - the seeder run twice
What happens when someone runs the seeder again? With the code above - a second batch of the same centurions and a database full of duplicates. The quartermaster must be prepared for this, and there are two strategies:
1public async run(dataSource: DataSource): Promise<void> {
2 const centurionRepository = dataSource.getRepository(Centurion);
3
4 // Strategy 1: clear the table before seeding
5 await centurionRepository.clear();
6
7 // Strategy 2: if the data is already there, do nothing
8 const existing = await centurionRepository.count();
9 if (existing > 0) {
10 return;
11 }
12
13 await centurionRepository.save([ /* ... */ ]);
14}In practice you pick one of them. clear() empties the table and seeds from scratch - the result is always predictable, so it is a good choice for development and test databases. The count() check leaves existing data untouched - safer wherever the database may already hold something valuable. Both versions share the property this is all about: the seeder can be run any number of times and the database will not turn into a pile of duplicates. This resistance to repeated runs is called idempotency - and I recommend treating it as an iron requirement of every seeder.
Order matters - relations between tables
A legionary in our system belongs to a centurion. That means we cannot seed legionaries into an empty database - their records point at centurions, which must exist first. The quartermaster loads the wagons in a fixed order:
1// 1. Independent entities first
2const centurions = await centurionRepository.save([
3 { name: 'Maximus Decimus', cohortName: 'Legio X Equestris' },
4]);
5
6// 2. Then the entities that reference them
7await legionaryRepository.save([
8 { name: 'Marcus Antonius', rank: 'Speculator', centurion: centurions[0] },
9 { name: 'Gaius Julius', rank: 'Optio', centurion: centurions[0] },
10]);Notice that save returns the saved objects - already carrying the identifiers the database assigned. That is what lets us pass centurions[0] as the relation's owner in the second step. The general rule reads: first the tables others point to (skills, ranks, centurions), then the dependent tables (legionaries), and finally the data that ties one to the other (tributes with an assigned owner). Cleaning runs in the reverse order - dependents first, independents last.
A data factory - a hundred legionaries for tests
Starting data we type by hand, because there is little of it. But for performance tests or pagination work you need hundreds of records - and nobody is going to invent them. That is what a factory is for: a recipe for one random object, which the @faker-js/faker library fills with made-up data:
1// factories/legionary.factory.ts
2import { setSeederFactory } from 'typeorm-extension';
3import { Legionary } from '../src/legionariusze/legionariusze.entity';
4
5export default setSeederFactory(Legionary, (faker) => {
6 const legionary = new Legionary();
7 legionary.name = faker.person.fullName();
8 legionary.rank = faker.helpers.arrayElement(['Legionary', 'Speculator', 'Optio']);
9 legionary.tributeCount = faker.number.int({ min: 0, max: 50 });
10 legionary.isActive = faker.datatype.boolean();
11 return legionary;
12});The factory describes what one random legionary looks like: a real-sounding name, a rank drawn from a list, a tribute counter within a given range. Multiplying is a single line in the seeder:
1const legionaryFactory = factoryManager.get(Legionary);
2await legionaryFactory.saveMany(100);And the castra is full: a hundred different legionaries, each with different data, zero manual work. Note the division of roles - the factory defines the shape of a single record, while the seeder decides how many are created and when.
Running it - wiring everything together
Finally we tell TypeORM where to look for seeds and factories. We extend the data source configuration with two fields:
1// data-source.ts
2const options: DataSourceOptions & SeederOptions = {
3 type: 'postgres',
4 database: 'legionariusze_cohort',
5 entities: ['src/**/*.entity{.ts,.js}'],
6 seeds: ['src/seeds/*{.ts,.js}'],
7 factories: ['src/factories/*{.ts,.js}'],
8};The seeds field points at the seeder classes, factories at the factory recipes. From now on a single terminal command, npm run seed:run, walks through all the seeds and equips the database. A fresh castra becomes a ready camp in seconds.
Summary
Your cohort will never march without supplies again:
- a seeder is code that loads starting data, fixtures are static data in files - the supply list and the quartermaster who fulfils it,
- a seeder must be idempotent: clear the table with
clear(), or skip seeding whencount()detects existing data, - with relations the order is sacred: independent entities first, dependent ones after - and cleaning in reverse,
- a factory with faker describes one random record, and
saveMany(100)multiplies it for tests, - the
seedsandfactoriesfields in the configuration tie it all into oneseed:runcommand.
In the next lesson we will take on data validation - since the treasury can now equip itself, it is time to make sure nothing spoiled gets inside. For now remember: a seeder is the cohort's quartermaster - thanks to it, every fresh database receives the same complete equipment with a single command.
Code for this lesson: src/seeders.ts
1// Seeders and Fixtures - Initial Cohort Cargo
2import { DataSource } from 'typeorm';
3
4console.log("Seeders - automatic loading of initial data!");
5
6// ===========================================
7// 1. Basic seeder
8// ===========================================
9
10export async function seedLegionaries(dataSource: DataSource): Promise<void> {
11 const legionaries = [
12 { name: 'Marcus Aurelius', rank: 'Legatus', experience: 15, legion: 'Legio X Gemina' },
13 { name: 'Gaius Julius', rank: 'Centurio', experience: 10, legion: 'Legio III Augusta' },
14 { name: 'Lucius Scipio', rank: 'Optio', experience: 7, legion: 'Legio IX Hispana' },
15 { name: 'Titus Flavius', rank: 'Miles', experience: 3, legion: 'Legio XII Fulminata' },
16 { name: 'Publius Decimus', rank: 'Miles', experience: 1, legion: 'Legio V Macedonica' },
17 ];
18
19 await dataSource
20 .createQueryBuilder()
21 .insert()
22 .into('legionaries')
23 .values(legionaries)
24 .execute();
25
26 console.log('Loaded ' + legionaries.length + ' legionaries!');
27}
28
29// ===========================================
30// 2. Province seeder
31// ===========================================
32
33export async function seedProvinces(dataSource: DataSource): Promise<void> {
34 const provinces = [
35 { name: 'Gallia', governor: 'Vercingetorix', population: 120000, tribute: 50000 },
36 { name: 'Aegyptus', governor: 'Cleopatra', population: 300000, tribute: 80000 },
37 { name: 'Britannia', governor: 'Boudicca', population: 80000, tribute: 20000 },
38 { name: 'Hispania', governor: 'Scipio', population: 150000, tribute: 45000 },
39 { name: 'Africa', governor: 'Jugurtha', population: 200000, tribute: 60000 },
40 ];
41
42 await dataSource
43 .createQueryBuilder()
44 .insert()
45 .into('provinciae')
46 .values(provinces)
47 .execute();
48
49 console.log('Loaded ' + provinces.length + ' provinces!');
50}
51
52// ===========================================
53// 3. Test data factory (Factory)
54// ===========================================
55
56const ROMAN_NAMES = ['Marcus', 'Gaius', 'Lucius', 'Titus', 'Publius'];
57const SURNAMES = ['Aurelius', 'Julius', 'Scipio', 'Flavius', 'Decimus'];
58const RANKS = ['Miles', 'Optio', 'Centurio', 'Tribunus', 'Legatus'];
59
60function createMockLegionary(overrides?: any) {
61 const randomFrom = (arr: string[]) => arr[Math.floor(Math.random() * arr.length)];
62
63 return {
64 id: Math.floor(Math.random() * 1000),
65 name: randomFrom(ROMAN_NAMES) + ' ' + randomFrom(SURNAMES),
66 rank: randomFrom(RANKS),
67 experience: Math.floor(Math.random() * 20),
68 legion: 'Legio ' + randomFrom(['X', 'III', 'IX', 'V', 'XII']),
69 ...overrides,
70 };
71}
72
73function createMockLegionaries(count: number) {
74 return Array.from({ length: count }, (_, i) =>
75 createMockLegionary({ id: i + 1 }),
76 );
77}
78
79// ===========================================
80// 4. Main runner
81// ===========================================
82
83// async function runSeeds() {
84// const dataSource = new DataSource({ ... });
85// await dataSource.initialize();
86// await seedLegionaries(dataSource);
87// await seedProvinces(dataSource);
88// await dataSource.destroy();
89// console.log('Seeding completed!');
90// }
91
92console.log("\n=== SEEDERS SUMMARY ===");
93console.log("Seeder - function loading initial data");
94console.log("Factory - generates random test data");
95console.log("Fixtures - static data in files");
96console.log("createQueryBuilder().insert().into().values() - inserting");
97console.log("Run seeds after migrations!");
98Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What are database seeders used for?
2. What is the main difference between fixtures and seeders?
Hands-on tasks in the game
- Vertical ordering
Arrange the steps of the database seeding process:
- Code editor
Create a seed() function that connects to the database, clears the legionaries table and inserts 5 new legionaries
- Click in order
Arrange the tables in the correct seeding order (tables without dependencies first):
- Horizontal ordering
Arrange the elements of clearing a table via a repository in the correct order:
- Code editor
Write a createMockLegionary(overrides?) function that returns a legionary object with default values overridden by overrides