A TypeORM migration is a versioned TypeScript class with an up() method that applies a deliberate, ordered database schema (or data) change, and typically a down() method that reverses it, checked into source control and run explicitly.
In simpler words
A migration is a committed, numbered record of exactly how the database changed, so every environment — your laptop, a teammate’s, staging, prod — ends up with the same schema in the same order.
This is the single most important backend habit in the course: schema changes are files you review, not something TypeORM infers silently.
Explain why synchronize is false everywhere in this repo, including locally
Run the generate → review → run → commit workflow with this repo’s scripts
Keep AppModule entities and data-source.ts entities in sync
State the rule for fixing a mistake in an already-applied migration
Sketch an expand/contract rollout for a risky column change
Why migrations beat synchronize
Definition
Schema synchronization (synchronize: true) has TypeORM compare entities to the live database and automatically alter tables to match, whereas migrations are explicit, reviewable, versioned files that apply the same steps everywhere.
In simpler words
Synchronize feels convenient alone on a throwaway database, but on any shared database it can silently drop columns or tables to make reality match the entities — with no chance to review the SQL first.
This repo sets synchronize: false in both AppModule and data-source.ts — there is no environment where it flips to true.
The course runs migrations locally too, on purpose, so the discipline is a habit before it ever matters on staging or prod.
Intermediate mindset: the database schema is a product contract shared by every environment. Treat migration SQL like production code.
The rule, in code
TypeOrmModule.forRoot({
// ...
synchronize: false, // schema changes ship as migration files, always
migrationsRun: false, // run explicitly via CLI in this course
})
Every schema change is a file under apps/api/src/database/migrations, reviewed like any other PR.
Two DataSources: AppModule vs CLI
Definition
The Nest runtime opens Postgres through TypeOrmModule.forRoot, while the TypeORM CLI uses a separate DataSource module (data-source.ts) that must list the same entities and also keep synchronize: false.
In simpler words
If entities diverge between AppModule and data-source.ts, generated migrations and runtime behavior disagree — a classic intermediate footgun.
apps/api/src/database/data-source.ts loads DATABASE_URL, entities [User, Ticket], and migrations path for TypeScript sources.
AppModule lists the same entities for the running API. When you add an entity, update both places in the same PR.
CLI scripts: pnpm migration:generate / migration:run / migration:revert inside @repo/api (often aliased from the monorepo root).
This file is for CLI only — Nest does not import it at runtime.
Day-to-day: generate, review, run, revert
Definition
The migration workflow is: change an entity, generate a migration by diffing entities against the current schema, read the generated SQL, run it against the database, and commit the entity change and the migration file together.
In simpler words
Generation is a starting draft, not a final answer — always read the SQL like you would review a teammate’s PR before running it.
Root-level pnpm scripts often alias into the API package: migration:generate, migration:run, migration:revert.
Review traps: dropped columns you did not intend, type changes that rewrite data, missing indexes for new filter columns, destructive defaults.
Run against a disposable database first when the SQL is scary. Prefer a backup or expand/contract on shared environments.
The commands, in order
pnpm --filter @repo/api migration:generate src/database/migrations/AddTicketPriority
# read the generated SQL before doing anything else
pnpm --filter @repo/api migration:run
pnpm --filter @repo/api migration:revert # undo the most recent batch if needed
Exact root aliases may wrap these — always confirm against package.json.
The template to study
apps/api/src/database/migrations/1710000000000-InitSchema.ts
// creates users + tickets, their constraints, and both indexes
Read this file end to end before writing your first migration — it is the “what good looks like” example.
Mistake: commit entity without migration
// Wrong — entity has new column; no migration file
// teammate pulls code → runtime queries fail against old schema
// Right — same PR: entity + migration + reviewed SQL
Code and schema must move together or every environment drifts.
Never edit an applied migration — expand/contract instead
Definition
Once a migration has run in a shared environment, that file becomes part of an immutable, ordered history that other environments already applied; editing it after the fact desynchronizes anyone who ran the original version.
In simpler words
If a migration turns out to be wrong, the fix is always a new migration, never a rewrite of the old file.
Environments track which migration files have already run (by name/timestamp) — editing history in place breaks that tracking silently.
For a risky change (like a rename), prefer expand/contract: add the new column, migrate the app and data, then remove the old column in a later migration — so old and new app versions can coexist mid-deploy.
Failed mid-migration: stop, inspect the DB, fix forward with a new migration or carefully restore from backup — do not hand-edit the migrations table unless you know exactly what you are doing.
Expand/contract sketch
-- migration 1: expand
ALTER TABLE tickets ADD COLUMN priority varchar(20) NULL DEFAULT 'normal';
-- deploy app that reads/writes priority
-- migration 2 (later): contract, only after the rollout is safe
ALTER TABLE tickets ALTER COLUMN priority SET NOT NULL;
Slower and more boring than one big ALTER — and it never breaks an in-flight deploy.
Indexes belong in migrations too
Definition
An index is a schema object that must be created and reviewed like any other column change; application code that filters without an index will look fine on seed data and fail in production scale.
In simpler words
When you add a filter/sort column used by list endpoints, add the supporting index in the same migration unless you have a measured reason not to.
InitSchema already demonstrates indexes — copy that discipline for new access patterns.
Concurrent index builds exist for large production tables; know they are an advanced ops concern, not an excuse to skip indexes.
Keep in mind
Read every generated migration’s SQL before running it — generators are smart, not perfect.
Keep AppModule entities and data-source.ts entities identical.
Index columns you will filter or sort on in the same migration that adds them.
A down() method is best-effort in production; do not assume every migration is cleanly reversible after real data has flowed through it.
Test
Check your understanding
At least 10 questions — mix of concept, syntax, practical, and logic. Score ≥ 80% (enforced by the API) to save progress.