Configure TypeORM migrations in 5 minutes
TypeORM still ships with NestJS templates and still works fine, but the ergonomics have aged badly compared to Drizzle and Prisma. If you're stuck on TypeORM in 2026 — usually because the codebase is too large to swap — here's a migration setup that doesn't fight you.
The shape of it
Two pieces:
AppModulewires TypeORM into the runtime so the app can talk to the databasetypeorm.config.tsis a standaloneDataSourcethe CLI uses formigration:generateandmigration:run
They share the same connection settings but live separately because the CLI can't import NestJS's DI container.
The runtime config
1// app.module.ts
2import { Module } from '@nestjs/common'
3import { TypeOrmModule } from '@nestjs/typeorm'
4import { ConfigModule, ConfigService } from '@nestjs/config'
5
6@Module({
7 imports: [
8 ConfigModule.forRoot({ isGlobal: true }),
9 TypeOrmModule.forRootAsync({
10 inject: [ConfigService],
11 useFactory: (config: ConfigService) => ({
12 type: 'postgres',
13 url: config.getOrThrow<string>('DB_URL'),
14 entities: ['dist/**/*.entity.js'],
15 migrations: ['dist/migrations/*.js'],
16 migrationsTableName: '_migrations',
17 migrationsRun: false,
18 synchronize: false,
19 logging: ['error', 'warn'],
20 }),
21 }),
22 ],
23})
24export class AppModule {}Three things worth calling out:
migrationshas no leading slash —dist/migrations/*.js, not/dist/.... A leading slash points at the filesystem root, which silently matches nothing.migrationsRun: false. Auto-running on boot looks convenient until you scale to multiple instances and two of them race the same migration. Run migrations as a deploy step instead.synchronize: false. Always. This setting drops and recreates tables to match your entities. It is not a "dev convenience" — it is a data-loss bug waiting for the day someone pushes the wrong env file. Treat it likerm -rfon your schema.
The CLI config
The TypeORM CLI doesn't run inside Nest, so it needs its own DataSource:
1// typeorm.config.ts
2import 'dotenv/config'
3import { DataSource } from 'typeorm'
4
5export default new DataSource({
6 type: 'postgres',
7 url: process.env.DB_URL,
8 entities: ['dist/**/*.entity.js'],
9 migrations: ['dist/migrations/*.js'],
10 migrationsTableName: '_migrations',
11})Note this points at compiled JS in dist/ — generate and run migrations against a built project, not raw .ts files. This avoids the whole "which TS loader does the CLI use" question.
package.json scripts
1{
2 "scripts": {
3 "typeorm": "typeorm-ts-node-commonjs",
4 "migration:generate": "bun run build && bun run typeorm migration:generate -d typeorm.config.ts",
5 "migration:run": "bun run build && bun run typeorm migration:run -d typeorm.config.ts",
6 "migration:revert": "bun run build && bun run typeorm migration:revert -d typeorm.config.ts",
7 "migration:create": "bun run typeorm migration:create"
8 }
9}typeorm-ts-node-commonjs ships with TypeORM and replaces the old ts-node ./node_modules/typeorm/cli dance. Use it instead of bare ts-node — ts-node is in maintenance mode in 2026 and most teams have migrated to tsx or just compile first.
Generating a new migration:
1bun run migration:generate ./src/migrations/AddEmailToUserTypeORM diffs your entities against the live database and writes the SQL needed to reconcile them. The class name (AddEmailToUser1730000000000) is what gets stored in _migrations, so make it unique and descriptive — that's the audit trail you'll be reading three years from now.
What migration:generate actually catches
It catches column adds, drops, type changes, index changes, and FK changes. It misses anything that isn't expressible as a schema diff: data backfills, table renames (it sees a drop + add), enum value renames, and anything involving extensions or triggers. Always read the generated file before committing it. Treat it as a draft.
Migrations run inside a transaction by default, so a failed migration rolls back cleanly — but only if every statement is transactional. CREATE INDEX CONCURRENTLY and most ALTER TYPE operations on enums aren't, and TypeORM will surface the Postgres error rather than smooth it over.
Running migrations on deploy
Run them as a separate step in your pipeline, not from the app process:
1bun run migration:runThen start the app. This means the schema is up to date before the new code boots, and it can't race with itself across replicas. If you must run migrations from the app, gate it behind a single-instance "migrator" container and leave migrationsRun: false everywhere else.
Should you still be using TypeORM?
Honestly — for greenfield work in 2026, probably not. Drizzle gives you a better TypeScript story, Prisma's tooling is years ahead, and both have more active communities. TypeORM's decorators-and-DataSource model is a 2019 idea that hasn't aged well. But if you're maintaining a NestJS codebase that already uses it, the setup above will keep migrations boring, which is the only thing you want migrations to be.