NestJS course Β· Module 3: TypeORM and Databases
Migrations and Entities - the Roman legion's archive
In this lesson6
You added a rank field to the Legionary entity. It works on your machine - the development database created the column by itself. You push the code, and the next morning a message arrives from a colleague: the application will not start, because his database has no rank column. On production it would be worse - nobody there lets a database rebuild itself, because along the way it can wipe out data.
The problem is the same one code had before git was invented: changes to the database structure have no history. Nobody knows who changed what, in what order, or how to undo it. The Romans kept an archive for exactly this - every change to the legion's register written down as a separate dated document. In TypeORM that document is called a migration.
What a migration is
A migration is a file holding one schema change - adding a table, a column, an index. Its name carries a timestamp, so migrations line up in order, and the database remembers which ones it has already run. This is schema versioning: exactly what git does for code, only for the structure of your tables.
Thanks to that, every colleague runs the same migrations in the same order and ends up with an identical database. And on production the change goes in deliberately, with one command, rather than by an ORM's guesswork. That is why synchronize: true - the option that lets TypeORM rebuild the database to match the entities - is convenient locally but must never reach production.
Anatomy of a migration: up and down
A migration is a class implementing the MigrationInterface interface with two methods:
1import { MigrationInterface, QueryRunner } from 'typeorm';
2
3export class CreateLegionaryTable1710000000000 implements MigrationInterface {
4 public async up(queryRunner: QueryRunner): Promise<void> {
5 await queryRunner.query(`
6 CREATE TABLE "legionary" (
7 "id" SERIAL PRIMARY KEY,
8 "name" character varying(100) NOT NULL,
9 "rank" character varying(50)
10 )
11 `);
12 }
13
14 public async down(queryRunner: QueryRunner): Promise<void> {
15 await queryRunner.query(`DROP TABLE "legionary"`);
16 }
17}These two methods are the road there and back. up() applies the change - here it creates the table. down() undoes it - it drops that same table. The rule is simple: down() must reverse exactly what up() did. Added a column in up? Drop it in down. Without that, reverting a migration leaves the database in a state nobody planned for.
Note the argument of both methods: queryRunner. It is the same object you will meet again in the lesson on transactions - it represents a database connection on which we execute commands. The number in the class name (1710000000000) is the creation timestamp; TypeORM orders migrations by it, not by the file name.
The migration workflow
In daily work you rarely write migrations by hand. The order is always the same and worth remembering as four steps:
1. Change the entity - add a field, change a type, remove a column. This is the only place where you describe what the database should look like.
2. Generate the migration with the command that compares the entities against the database and records the difference:
1typeorm migration:generate -n AddRankToLegionary3. Run the migration - only now does the change reach the database:
1typeorm migration:run4. Check the result in the database - is the column there, does it have the right type, did the data survive.
Between steps two and three sits something easy to forget: read the generated file. The generator compares entities with the database and sometimes interprets a change differently than you intended - instead of renaming a column, it may drop the old one and add a new one, losing all the data on the way. That review takes half a minute, and I recommend it as a habit.
generate vs create - two different commands
It is worth separating two commands with misleadingly similar names. migration:generate compares the entities with the database and fills in up() and down() for you - this is the command you will use in 95% of cases. migration:create creates an empty migration skeleton which you fill in yourself - useful when the change does not follow from the entities, for instance when moving data between columns.
When a migration turns out to be wrong, you undo it with one command:
1typeorm migration:revertrevert calls the down() method of the last executed migration - one, not all of them. To step back three migrations, you run it three times. And here you see the point of that discipline in writing down(): the rollback will work exactly as well as you described the road back.
The entity steers the migration - column options
Since migrations are born from entities, column options are really instructions for the generator. A few are worth knowing, because they decide the shape of the table directly:
1@Entity()
2export class Tribute {
3 @PrimaryGeneratedColumn()
4 id: number;
5
6 @Column({ type: 'varchar', length: 100 })
7 name: string;
8
9 @Column({ type: 'decimal', precision: 10, scale: 2 })
10 value: number;
11
12 @Column({ default: false })
13 isCursed: boolean;
14}@PrimaryGeneratedColumn() is a primary key assigned automatically by the database - you never set it by hand. type: 'varchar' with length: 100 gives text of limited length; an attempt to save a longer name is rejected by the database rather than silently truncated.
The most interesting is decimal with the precision and scale pair, because that pair tends to confuse. precision is the total number of digits, and scale is how many of them sit after the decimal point. So precision: 10, scale: 2 means eight digits before the point and two after it - values up to 99,999,999.99. Why not a plain float? Because floating-point numbers round - with money and tribute values the totals would stop adding up. decimal stores the exact value.
default: false writes a default value into the column definition in the database - a new tribute will be uncursed even if the code never sets that field.
Summary
The legion's archive keeps itself, and you know how to read it:
- a migration is schema versioning - git for the structure of your tables,
synchronize: truecan be convenient locally, but on production it can wipe out data,- a migration class implements
MigrationInterface:up()applies the change,down()must reverse it exactly, - the workflow: change the entity,
migration:generate, read the generated file,migration:run, check the database, generatefills a migration from the entity-database difference,creategives an empty skeleton to write by hand,migration:revertundoes one, the last migration, by calling itsdown(),- column options steer the generator:
lengthlimits text,decimalwithprecisionandscalestores exact amounts,defaultwrites a default value into the database.
In the next lesson we will connect the tables with relations - because a legionary with no centurion and no tribute is still a lonely entry in the register. For now remember: a migration is a document in the archive - it describes one change and the road back, so that every database in the Empire can reach the same state.
Code for this lesson: src/migrations.ts
1// Migrations and Entity - Roman Legion Archive
2import { MigrationInterface, QueryRunner, Table, TableColumn } from 'typeorm';
3
4console.log("Migrations - safe reorganization of the treasury!");
5
6// ===========================================
7// 1. Creating table - first migration
8// ===========================================
9
10export class CreateLegionaries1700000000000 implements MigrationInterface {
11 async up(queryRunner: QueryRunner): Promise<void> {
12 await queryRunner.createTable(
13 new Table({
14 name: 'legionaries',
15 columns: [
16 {
17 name: 'id',
18 type: 'integer',
19 isPrimary: true,
20 isGenerated: true,
21 generationStrategy: 'increment',
22 },
23 { name: 'name', type: 'varchar', isNullable: false },
24 { name: 'rank', type: 'varchar', isNullable: false },
25 { name: 'experience', type: 'integer', default: '0' },
26 { name: 'legion', type: 'varchar', isNullable: false },
27 { name: 'created_at', type: 'timestamp', default: 'now()' },
28 ],
29 }),
30 );
31 console.log('Table legionaries created!');
32 }
33
34 async down(queryRunner: QueryRunner): Promise<void> {
35 await queryRunner.dropTable('legionaries');
36 console.log('Table legionaries deleted!');
37 }
38}
39
40// ===========================================
41// 2. Adding a column - next migration
42// ===========================================
43
44export class AddProvinceToLegionaries1700000001000 implements MigrationInterface {
45 async up(queryRunner: QueryRunner): Promise<void> {
46 await queryRunner.addColumn(
47 'legionaries',
48 new TableColumn({
49 name: 'province',
50 type: 'varchar',
51 isNullable: true,
52 default: "'Roma'",
53 }),
54 );
55 console.log('Added column province!');
56 }
57
58 async down(queryRunner: QueryRunner): Promise<void> {
59 await queryRunner.dropColumn('legionaries', 'province');
60 }
61}
62
63// ===========================================
64// 3. Migration commands
65// ===========================================
66
67// npx typeorm migration:generate -n CreateLegionaries
68// npx typeorm migration:run // Run migration
69// npx typeorm migration:revert // Revert the last one
70// npx typeorm migration:show // Show status
71
72// ===========================================
73// 4. Migration configuration
74// ===========================================
75
76// ormconfig.ts / data-source.ts
77// export const dataSource = new DataSource({
78// type: 'postgres',
79// host: 'localhost',
80// port: 5432,
81// migrations: ['src/migrations/*.ts'],
82// migrationsTableName: 'typeorm_migrations',
83// });
84
85console.log("\n=== MIGRATIONS SUMMARY ===");
86console.log("up() - executes change (creating table, adding column)");
87console.log("down() - reverts the change (dropping a table, column)");
88console.log("migration:run - runs pending migrations");
89console.log("migration:revert - reverts the last migration");
90console.log("synchronize: true ONLY in development!");
91Spotted 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 migrations used for in TypeORM?
2. Which command generates a migration based on changes in TypeORM entities?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Create a CreateLegionaryTable migration with an up() method that creates the table and a down() method that drops it
- Vertical ordering
Arrange the steps of the database migration process from first to last:
- Click in order
Arrange the elements of the migration generation command in the correct order:
- Code editor
Write a Tribute entity with @Column({ type: 'varchar', length: 100 }), @Column({ type: 'decimal', precision: 10, scale: 2 }) and @Column({ default: false })