Home Database TypeORM — synchronize: true Wiped a Production Table
Intermediate 6 min · March 17, 2026
TypeORM Basics

TypeORM — synchronize: true Wiped a Production Table

TypeORM's synchronize: true silently dropped a 'created_at' column in production, wiping timestamps.

N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 25 min
  • Solid grasp of fundamentals
  • Comfortable reading code examples
  • Basic production concepts
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • TypeORM maps TypeScript classes to database tables via decorators
  • DataSource.initialize() establishes the connection pool
  • Repository API covers 80% of CRUD; QueryBuilder for complex joins
  • Relations are lazy by default — must explicitly load with 'relations' option
  • Never use synchronize: true in production — use migrations to avoid data loss
✦ Definition~90s read
What is TypeORM Basics?

TypeORM is an Object-Relational Mapper (ORM) for TypeScript and JavaScript that maps database tables to classes (entities) and lets you query them using TypeScript syntax instead of raw SQL. It supports MySQL, PostgreSQL, SQLite, and others, and is one of the most popular ORMs in the Node.js ecosystem alongside Prisma and Sequelize.

TypeORM lets you work with databases using TypeScript classes instead of SQL.

TypeORM's key value is reducing boilerplate for CRUD operations and providing type-safe queries, but it also introduces abstraction layers that can hide dangerous defaults — most notably the synchronize: true flag, which automatically alters your database schema to match your entity definitions on every application startup.

You'd use TypeORM when you want rapid prototyping, a familiar ActiveRecord-like pattern, or deep integration with TypeScript decorators. Avoid it if you need fine-grained control over SQL, work with legacy schemas that don't map cleanly to entities, or prefer a query-builder approach like Knex.js.

The synchronize flag is convenient in development but catastrophic in production — it can drop columns, rename tables, or wipe data without warning, as the article's title warns. For production, you must use TypeORM's migration system, which generates versioned SQL files you review and apply manually.

TypeORM's entity system uses decorators like @Entity(), @Column(), and @PrimaryGeneratedColumn() to define schemas. Its repository pattern (getRepository(User)) provides methods like find(), save(), and delete(). Relations (@ManyToOne, @OneToMany) support eager/lazy loading, but lazy loading often triggers N+1 queries.

The migration CLI (typeorm migration:generate) creates timestamped SQL files from entity changes, letting you evolve schemas safely — the only sane approach for production databases.

Plain-English First

TypeORM lets you work with databases using TypeScript classes instead of SQL. You define a class like User, add some decorators, and TypeORM handles creating tables, inserting data, and querying — all with type safety and autocomplete.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

TypeORM is the most popular ORM for TypeScript/Node.js applications. It follows the Active Record or Data Mapper pattern, letting you interact with your database using TypeScript classes and decorators rather than writing raw SQL.

The main advantage over raw queries: type safety. Refactoring column names propagates through the application with TypeScript compiler errors, not runtime bugs. The main disadvantage: complex queries are often easier to read as SQL than as QueryBuilder chains.

What TypeORM's Synchronize Flag Actually Does

TypeORM's `synchronize: true` is a development convenience that automatically creates database tables and columns based on your entity definitions at application startup. It mirrors your entity schema to the database schema on every application boot, effectively running DDL statements like CREATE TABLE, ALTER TABLE, and ADD COLUMN without explicit migrations.

In practice, this means TypeORM reads your entity decorators (e.g., @Entity, @Column) and generates corresponding SQL DDL. If you add a new column to an entity, synchronize will ALTER TABLE to add it. If you rename a column, synchronize will drop the old column and create a new one — losing all data in that column. It does not perform data migrations; it only ensures the schema structure matches the entity definitions at that moment.

Use `synchronize: true` only in local development or isolated test environments where data loss is acceptable. In any shared environment — staging, QA, or production — disable it and use proper migration scripts. The flag is not a substitute for version-controlled migrations; it is a rapid prototyping tool that becomes a liability the moment you have real data.

⚠ Synchronize is not a migration tool
It drops columns and tables without warning. A renamed entity field becomes a dropped column — data gone, no rollback.
📊 Production Insight
A team enabled synchronize: true in a staging environment shared with QA. A developer renamed a column in the entity, and on next deploy, TypeORM dropped the old column and created a new empty one — wiping 3 months of QA test data. The team had no backup because they assumed staging was transient. Rule: never enable synchronize on any database that holds data you cannot afford to lose, even if it's "just" staging.
🎯 Key Takeaway
Synchronize is for prototyping only — never for environments with persistent data.
It performs destructive schema changes (drop column, drop table) without confirmation.
Always use version-controlled migrations for any schema change in shared or production databases.
typeorm-basics TypeORM Schema Management Layers From entity definitions to safe production deployment Application Layer Entity Definitions | Repository Pattern | Query Builder TypeORM Core Synchronize Flag | Migration Runner | Schema Builder Schema Evolution Auto-Sync (Dev) | Migrations (Prod) | Seed Data Database Layer Production Tables | Staging Tables | Development Tables THECODEFORGE.IO
thecodeforge.io
Typeorm Basics

Setting Up TypeORM

The DataSource is the central hub of TypeORM — it holds the connection pool and entity registry. You configure it once at startup, usually in a separate file. The synchronize option should always be false in production; use migrations for schema evolution.

data-source.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { DataSource } from 'typeorm';
import { User } from './entities/User';
import { Post } from './entities/Post';

export const AppDataSource = new DataSource({
    type: 'postgres',
    host: 'localhost',
    port: 5432,
    username: 'postgres',
    password: 'password',
    database: 'myapp',
    entities: [User, Post],
    synchronize: false,       // NEVER use true in production — use migrations
    logging: ['query', 'error'],
});

// Initialize once at app startup
await AppDataSource.initialize();
console.log('Database connected');
Try it live
⚠ Don't forget to close the DataSource during shutdown
If you don't call await AppDataSource.destroy() on app shutdown, you'll leave open connections in the pool, causing a slow leak. Use process.on('SIGTERM', ...) in serverless or containers.
📊 Production Insight
A misconfigured DataSource can take down the whole app if the database is unreachable.
Use a retry mechanism with exponential backoff instead of failing instantly.
Rule: never embed credentials in source code — use environment variables.
🎯 Key Takeaway
DataSource is a singleton — initialize once, reuse everywhere.
Synchronize false is the first line of defense against accidental data loss.
Always pair with a shutdown hook.
Choosing DataSource config approach
IfSingle database, simple app
UseUse default config in code. Just set type, host, credentials.
IfMultiple databases or complex pool tuning
UseUse ormconfig.json or environment-specific config files.
IfServerless (AWS Lambda, etc.)
UseCreate DataSource lazily and cache across invocations — never create per request.

Defining Entities

Entities are TypeScript classes decorated with @Entity. Each entity maps to a database table. Columns are defined with @Column, @PrimaryGeneratedColumn, etc. Relations use @OneToMany, @ManyToOne, @ManyToMany, and @JoinColumn to define foreign keys. Always specify the inverse side for bidirectional relations.

entities/User.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, OneToMany } from 'typeorm';
import { Post } from './Post';

@Entity('users')
export class User {
    @PrimaryGeneratedColumn()
    id: number;

    @Column({ unique: true })
    email: string;

    @Column()
    name: string;

    @Column({ default: true })
    isActive: boolean;

    @CreateDateColumn()
    createdAt: Date;

    @OneToMany(() => Post, (post) => post.author)
    posts: Post[];
}
Try it live
Mental Model
Entity = DTO + Schema definition
Think of an entity as both the blueprint for the table and the shape of the data you'll work with in your app.
  • The decorators tell TypeORM what SQL DDL to generate (create table, add foreign key).
  • The class is used at runtime to hold query results.
  • Changing a decorator does not alter the database unless you run a migration.
📊 Production Insight
Adding @Column() with a default value does not automatically backfill existing rows.
You must write a migration to ALTER COLUMN SET DEFAULT or update rows manually.
Rule: always verify migration SQL against production data shape.
🎯 Key Takeaway
Entity decorators are declarative schema definitions, not runtime mutations.
Relations need explicit inverse side for TypeORM to manage foreign keys.
Test every new column against real data volume before deploying.
Choosing column types
IfPrimary key with auto-increment
UseUse @PrimaryGeneratedColumn()
IfUUID primary key
UseUse @PrimaryGeneratedColumn('uuid') or set default in database.
IfTimestamps like createdAt/updatedAt
UseUse @CreateDateColumn and @UpdateDateColumn — they auto-set on persist.
typeorm-basics THECODEFORGE.IO TypeORM Schema Management Architecture Layered components for safe schema evolution Application Layer Entity Definitions | Repository Pattern | Query Builder ORM Layer TypeORM Core | Synchronize Flag | Migration Runner Schema Management Auto-Sync (dev) | Migration Files | Schema Versioning Database Layer Production DB | Staging DB | Development DB Safety Controls Environment Detection | Migration Guard | Audit Logs THECODEFORGE.IO
thecodeforge.io
Typeorm Basics

CRUD with Repository

The Repository pattern is the simplest way to interact with a single entity. Use dataSource.getRepository(Entity) to get a repository instance. For simple operations like create, read, update, delete, the repository methods are enough. For complex queries — joins, aggregations, subqueries — switch to QueryBuilder.

service/user.service.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const userRepo = AppDataSource.getRepository(User);

// CREATE
const user = userRepo.create({ name: 'Alice', email: 'alice@example.com' });
await userRepo.save(user);
console.log('Created user:', user.id);

// READ
const found = await userRepo.findOne({ where: { email: 'alice@example.com' } });
const all   = await userRepo.find({ where: { isActive: true } });

// READ with relations
const withPosts = await userRepo.findOne({
    where: { id: 1 },
    relations: { posts: true }
});
console.log(withPosts?.posts.length);

// UPDATE
await userRepo.update({ id: 1 }, { name: 'Alice Smith' });

// DELETE
await userRepo.delete({ id: 1 });

// QueryBuilder for complex queries
const activeUsers = await userRepo
    .createQueryBuilder('user')
    .leftJoinAndSelect('user.posts', 'post')
    .where('user.isActive = :active', { active: true })
    .andWhere('post.id IS NOT NULL')
    .orderBy('user.createdAt', 'DESC')
    .getMany();
Try it live
🔥save() vs update()
save() triggers lifecycle hooks and cascades; update() is a direct SQL UPDATE. Use save() for single entity operations that need hooks. Use update() for bulk updates where you don't need events.
📊 Production Insight
findOne returns null when not found — not undefined. Accessing properties without check throws TypeError.
The relations option triggers an extra JOIN per relation — limit to only those you need.
Rule: always check for null after findOne and use relations: { ... } selectively.
🎯 Key Takeaway
Repository API is your default for CRUD — fast, typed, and straightforward.
Switch to QueryBuilder when you need WHERE on related tables or aggregation.
Never nest find calls inside loops — that's the N+1 problem.
Choosing between Repository and QueryBuilder
IfSimple CRUD on one table
UseUse Repository API (find, save, delete).
IfComplex query with joins, subqueries, or aggregations
UseUse createQueryBuilder.
IfNeed raw SQL performance or database-specific features
UseUse dataSource.query('SELECT ...') — but lose type safety.

Relations: Loading Strategies and Pitfalls

TypeORM relations are lazy by default — they don't load related entities unless you explicitly ask for them. You can load relations via the 'relations' option in find/findOne, or via leftJoinAndSelect in QueryBuilder. Eager relations (set with { eager: true }) load automatically but can cause N+1 if used carelessly. The biggest production pitfall: assuming relations are always loaded.

relations-example.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Lazy loading (default) — need to specify relations
const user = await userRepo.findOne({ where: { id: 1 } });
console.log(user.posts); // undefined — not loaded

// Eager loading
const user = await userRepo.findOne({
    where: { id: 1 },
    relations: { posts: true }
});
console.log(user.posts); // loaded

// QueryBuilder loading
const user = await userRepo
    .createQueryBuilder('user')
    .leftJoinAndSelect('user.posts', 'post')
    .where('user.id = :id', { id: 1 })
    .getOne();
console.log(user.posts); // loaded
Try it live
⚠ Eager loading on OneToMany can be a performance trap
If you mark a OneToMany relation as eager, every find() on that entity will load all related records — even when you don't need them. This can easily become the source of slow queries. Always measure the actual query log.
📊 Production Insight
N+1 queries happen when you iterate over a collection and access a lazy relation inside the loop.
The fix: use leftJoinAndSelect in a single query instead of separate queries per item.
Rule: profile your API endpoints — if response time grows linearly with data volume, you likely have N+1.
🎯 Key Takeaway
Relations are lazy — always load explicitly.
Eager loading is convenient but dangerous on OneToMany.
Use leftJoinAndSelect for complex filtering across relations.
When to use eager vs lazy relations
IfRelation always needed (e.g., User -> Profile)
UseConsider eager: true on the owning side.
IfRelation rarely needed (e.g., User -> many Posts)
UseKeep lazy; load on demand with relations option.
IfNeed to filter on related table properties
UseUse QueryBuilder with leftJoinAndSelect.

Migrations: Safe Schema Evolution

Migrations are the only safe way to change your database schema in production. TypeORM's migration system generates SQL files based on entity changes. Always review the generated SQL before running. Use migration:generate to create a migration, then migration:run to apply. Never edit migration files manually unless you understand the full impact.

migration-commands.shBASH
1
2
3
4
5
6
7
8
9
10
11
# Generate a migration after changing entities
npx typeorm migration:generate -d src/data-source.ts src/migrations/AddEmailVerifiedColumn

# Run pending migrations
npx typeorm migration:run -d src/data-source.ts

# Revert last migration
npx typeorm migration:revert -d src/data-source.ts

# Show status
npx typeorm migration:show -d src/data-source.ts
Mental Model
Migrations = Version control for your database
Just as you wouldn't push code without code review, you shouldn't apply schema changes without reviewing the SQL.
  • Each migration is a timestamped file that contains up and down methods.
  • TypeORM compares current entity definitions against the database to generate the diff.
  • Always verify the generated SQL — it may drop columns or tables unintendedly.
📊 Production Insight
Running multiple migrations in sequence can cause locking on large tables.
Always test migrations on a staging copy of production data first.
Rule: keep migrations small and atomic — one change per migration file.
🎯 Key Takeaway
Migrations are your safety net — use them, never synchronize.
Always review generated SQL before applying.
Test migrations on staging with production-like data volume.
When to run migrations
IfAdding a new column
UseGenerate migration, manually add ALTER COLUMN SET DEFAULT if needed.
IfRemoving a column
UseEnsure no application code references it first; migration will DROP COLUMN.
IfChanging column type
UseRequires careful handling: may need USING clause. Always test on staging.

Who the Hell Is This For? (Audience)

You're already debugging a JOIN that's pulling 40MB of JSON into an Express route at 2 AM. That's who this is for.

TypeORM isn't a toy. It's an ORM that'll happily let you shoot your own foot if you treat it like a magical black box. This blog is for the developer who's been burned by an ORM before — someone who wants to know why synchronize: true just wrote 47 unintended indexes to production, not just how to flip the flag.

If you're still writing findOne with no where clause and hoping for the best, close the tab. Come back when you've cleaned up a migration conflict at 3 AM. Otherwise, keep reading — we're about to talk about the parts of TypeORM that the docs skip because they'd rather sell you a happy story.

🎯 Key Takeaway
This blog assumes you've already broken something with an ORM before. If you haven't, you will — read the warnings.

What You’d Better Know Before Touching This Shit (Prerequisites)

TypeORM is a thin wrapper over SQL and TypeScript. If you can't write a raw SQL JOIN without panicking, you're going to have a bad time. Before you start wiring up entities and decorators, nail these:

  • TypeScript basics — generics, decorators, async/await. If typeof vs instanceof confuses you, go read the handbook.
  • SQL fundamentals — SELECT, INSERT, UPDATE, DELETE, and what an index actually does. TypeORM generates SQL; you need to read the query log to know when it's generating garbage.
  • Relational database design — normalisation, foreign keys, cascade deletes (and why they're a trap). The ORM will enforce relations only if you let it.
  • Node.js runtime — module resolution, transpilation, process lifecycle. TypeORM runs in Node, so know your event loop from your callback queue.

If you're missing any of these, save yourself the headache. Install SQLite, write a few raw queries, then come back. You'll thank me when your first migration doesn't nuke the user table.

PrerequisitesCheck.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — database tutorial

-- Can you read this query and predict its execution plan?
SELECT 
  u.id,
  u.email,
  o.total_amount,
  o.created_at
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.created_at > '2024-01-01'
  AND u.is_active = true
ORDER BY o.total_amount DESC;

-- If not, go learn SQL first. TypeORM hides complexity, not competence.
Output
-- (No output — this is a self-test. If you can't reason about the query, you're not ready.)
⚠ Production Trap:
TypeORM's query builder lets you stack .where() calls that override each other silently. If you don't understand SQL, you'll never catch it until the wrong data leaks into a report.
🎯 Key Takeaway
The ORM is a tool for productive devs, not a crutch for SQL-illiterate ones. Know the underlying database or prepare for pain.

Operators in TypeORM Query Builder

TypeORM's Query Builder gives you fine-grained control over SQL operators. Without them, you're stuck with basic equality queries. Operators let you filter by patterns (LIKE), ranges (BETWEEN), inclusion (IN), and null checks (IS NULL). Each operator maps directly to a Query Builder method: .where('age BETWEEN :min AND :max') or .andWhere('name LIKE :name'). The key insight: operator methods return the same query builder, so you chain them. Use :param placeholders instead of string interpolation to prevent injection. Start with Where, add AndWhere or OrWhere for compound conditions. The NOT operator inverts: .notBrackets() wraps exclusions. Logical operators combine with brackets for precedence. This replaces writing raw SQL strings while keeping full expressiveness.

OperatorsExample.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// io.thecodeforge — database tutorial

-- TypeORM Query Builder operators
WHERE age BETWEEN :minAge AND :maxAge
AND name LIKE :searchPattern
AND role IN (:...roles)
AND deletedAt IS NULL
AND (status = 'active' OR status = 'pending')

// Parameterized: safer than string concat
.createQueryBuilder('user')
.where('user.age BETWEEN :min AND :max', { min: 18, max: 65 })
.andWhere('user.name LIKE :name', { name: '%John%' })
.andWhere('user.role IN (:...roles)', { roles: ['admin', 'mod'] })
Output
Returns filtered records from user table
⚠ Production Trap:
Never embed user input directly into operator strings. Always use parameterized :param syntax to avoid SQL injection—TypeORM will escape values.
🎯 Key Takeaway
Use parameterized operator methods (andWhere, orWhere) with named placeholders—never raw interpolation.

Working Directory Gotchas with TypeOrm

TypeORM resolves entity paths, migration files, and config relative to the process working directory—not your project root. Run npm run from a subfolder? TypeORM might fail to find entity/*.ts. Fix it: set process.env.NODE_PATH or use absolute paths in ormconfig.ts. The root directory determines where TypeORM writes migration files and reads entities. Migration g:co generates files in the current working directory. If your CI runs from a different directory, migrations won't match. Always store TypeORM config with __dirname-based paths to decouple from runtime cwd. Test by running commands from random directories. Common failure: No entity metadata found because entity glob resolves to a wrong relative path.

WorkingDirFix.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — database tutorial

-- Use __dirname for reliable path resolution
import { DataSource } from 'typeorm';
import path from 'path';

export const AppDataSource = new DataSource({
  type: 'postgres',
  entities: [path.join(__dirname, 'entity/*.{ts,js}')],
  migrations: [path.join(__dirname, 'migration/*.{ts,js}')],
  // Never rely on relative './' — fails on different cwd
});

// Test: run from /app/src/ but cwd is /app/
// With above: 'entity/*.ts' resolves correctly
Output
DataSource initialized with absolute entity paths
⚠ Production Trap:
If migration files appear in unexpected folders, your cwd is off. Pin with __dirname or set NODE_PATH explicitly in all launch scripts.
🎯 Key Takeaway
Always resolve file paths with __dirname in TypeORM config to survive working directory changes.

Importing from TypeORM Correctly

TypeORM exports dozens of decorators, functions, and types. Importing the wrong thing causes cryptic errors. Core rule: Entity, Column, PrimaryGeneratedColumn come from typeorm main package. Repository methods (find, save, delete) live on Repository type, also from typeorm. The biggest trap: importing EntityRepository (deprecated) instead of using @Injectable() in NestJS or dataSource.getRepository(). For Query Builder, import SelectQueryBuilder from typeorm only when you need its type. For runtime usage, the actual object is returned by createQueryBuilder(). Avoid star imports (import * as ...) — they pull everything. Use named imports with only what you need: import { Entity, Column, BaseEntity } from 'typeorm'.

ImportExample.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge — database tutorial

// Correct imports (TypeORM 0.3+)
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  ManyToOne,
  JoinColumn,
  Repository,
} from 'typeorm';

// Wrong: cannot use deprecated EntityRepository
// const repo = await getRepository(User); // old API

// Right: use dataSource.getRepository()
const userRepo: Repository<User> = dataSource.getRepository(User);
const users = await userRepo.find({ where: { isActive: true } });
Output
No import errors — repository queries succeed
⚠ Production Trap:
Importing EntityRepository or getRepository (deprecated in 0.3) will fail silently or throw runtime errors. Always use dataSource.getRepository().
🎯 Key Takeaway
Use targeted named imports from 'typeorm' and dataSource.getRepository() for runtime repository access.

TypeORM in 2026: Current State and Migration to Prisma or Drizzle

As of 2026, TypeORM remains a widely-used ORM for TypeScript and JavaScript, but its ecosystem has evolved significantly. While TypeORM still receives maintenance updates, newer ORMs like Prisma and Drizzle have gained traction due to their type safety, performance, and developer experience. Prisma offers a declarative schema language and auto-generated queries, while Drizzle provides a lightweight, SQL-like query builder with full TypeScript support. If you are considering migrating from TypeORM, evaluate factors such as query complexity, migration tooling, and team familiarity. For example, migrating a simple CRUD app to Prisma involves rewriting entity definitions as Prisma schema and replacing repository calls with Prisma Client. A migration to Drizzle might require converting TypeORM query builders to Drizzle's SQL-like syntax. Both Prisma and Drizzle support PostgreSQL, MySQL, SQLite, and other databases. However, TypeORM still excels in complex relationships and legacy codebases. The decision should be based on project requirements, not hype. Below is an example of a TypeORM entity and its equivalent in Prisma and Drizzle.

migration-example.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- TypeORM Entity (for reference)
CREATE TABLE "user" (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL
);

-- Prisma Schema equivalent
-- model User {
--   id    Int     @id @default(autoincrement())
--   name  String
--   email String  @unique
-- }

-- Drizzle Schema equivalent
-- import { pgTable, serial, varchar } from 'drizzle-orm/pg-core';
-- export const users = pgTable('users', {
--   id: serial('id').primaryKey(),
--   name: varchar('name', { length: 255 }).notNull(),
--   email: varchar('email', { length: 255 }).notNull().unique(),
-- });
🔥Migration Complexity
📊 Production Insight
In production, avoid mixing ORMs. If migrating, use feature flags to route traffic to the new ORM incrementally, and monitor for regressions in query performance and data integrity.
🎯 Key Takeaway
TypeORM is still viable in 2026, but Prisma and Drizzle offer better type safety and performance for new projects. Migration requires careful planning and testing.

TypeORM Data Mapper vs Active Record Patterns

TypeORM supports both the Data Mapper and Active Record patterns, each with distinct trade-offs. In the Active Record pattern, entities themselves contain methods for persistence (e.g., user.save(), user.remove()). This pattern is simpler and intuitive for small projects but can lead to tight coupling between business logic and database access. In contrast, the Data Mapper pattern separates entities (plain objects) from repositories that handle persistence. This promotes cleaner separation of concerns and testability, especially in larger applications. TypeORM's default is Active Record, but you can switch to Data Mapper by defining repositories. For example, with Active Record, you define an entity extending BaseEntity: class User extends BaseEntity { ... }. With Data Mapper, you define a plain entity and a repository: const userRepository = dataSource.getRepository(User). The choice impacts scalability and maintainability. In production, Data Mapper is generally preferred for complex domains, while Active Record can be acceptable for simple CRUD apps. Below is a comparison of both patterns in TypeORM.

patterns-comparison.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
-- Active Record Pattern (Entity with save method)
-- CREATE TABLE "user" (
--     id SERIAL PRIMARY KEY,
--     name VARCHAR(255) NOT NULL
-- );
-- In code: const user = new User(); user.name = 'John'; await user.save();

-- Data Mapper Pattern (Repository-based)
-- Same table, but in code:
-- const user = new User(); user.name = 'John';
-- await userRepository.save(user);
💡Pattern Selection
📊 Production Insight
In production, Data Mapper allows you to swap database implementations or add caching layers without modifying entity code. Active Record can lead to scattered database calls that are harder to optimize.
🎯 Key Takeaway
Data Mapper separates business logic from persistence, improving testability and maintainability. Active Record is simpler but couples entities to the database.
Synchronize vs Migrations: Schema Management Trade-offs between auto-sync and manual migrations synchronize: true Migrations Schema Updates Automatic on app start Manual via migration commands Production Safety Risky: may drop tables Safe: version-controlled changes Rollback Support No rollback capability Yes, revert to previous version Audit Trail No history of changes Full change log in files Use Case Quick prototyping only Production and team workflows THECODEFORGE.IO
thecodeforge.io
Typeorm Basics

TypeORM Migrations: Production Deployment Workflow

Running migrations in production requires a careful workflow to avoid data loss or downtime. TypeORM migrations are JavaScript/TypeScript files that define up and down methods. The typical production workflow involves: (1) Generate a migration using typeorm migration:create or typeorm migration:generate after schema changes. (2) Review the generated SQL for correctness, especially for destructive operations like DROP COLUMN or ALTER TABLE. (3) Run migrations in a staging environment first. (4) In production, use a CI/CD pipeline that runs migrations as a separate step before deploying the application code. Never use synchronize: true in production. Instead, rely on migrations. For zero-downtime deployments, consider using expand-contract pattern: add new columns without removing old ones, then migrate data, and finally remove old columns in a later migration. Always back up the database before running migrations. Example migration file below.

production-migration.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- Example migration: Add 'age' column to 'user' table
-- Up migration
ALTER TABLE "user" ADD COLUMN "age" integer NOT NULL DEFAULT 0;

-- Down migration (rollback)
ALTER TABLE "user" DROP COLUMN "age";

-- In TypeORM migration file:
-- import { MigrationInterface, QueryRunner } from "typeorm";
-- export class AddAgeColumn1234567890123 implements MigrationInterface {
--     name = 'AddAgeColumn1234567890123';
--     public async up(queryRunner: QueryRunner): Promise<void> {
--         await queryRunner.query(`ALTER TABLE "user" ADD "age" integer NOT NULL DEFAULT '0'`);
--     }
--     public async down(queryRunner: QueryRunner): Promise<void> {
--         await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "age"`);
--     }
-- }
⚠ Migration Safety
📊 Production Insight
Automate migration execution in your deployment pipeline. Use database migration tools like Flyway or Liquibase for complex environments. Monitor migration duration and set timeouts to prevent long-running locks.
🎯 Key Takeaway
Use migrations instead of synchronize in production. Follow a CI/CD workflow with staging testing, backups, and rollback plans.
● Production incidentPOST-MORTEMseverity: high

Synchronize: True Wiped a Production Table

Symptom
Users reported missing 'created_at' values on all records created before the deployment. The column simply vanished from the table.
Assumption
The team assumed synchronize would only add missing columns, not remove existing ones. They didn't review the generated SQL.
Root cause
TypeORM's synchronize option compares entity schemas to the database and executes DDL to make them match — including DROP COLUMN for removed decorators. No confirmation prompt.
Fix
1. Restore the column from a backup. 2. Set synchronize: false in all DataSource configs. 3. Move to a migration workflow: generate, review, then run.
Key lesson
  • Never use synchronize: true in any environment connected to real data.
  • Always review migration SQL before applying — even in staging.
  • Use timestamp columns carefully — they are often removed first during refactors.
Production debug guideSymptom → Action guide for common TypeORM failures5 entries
Symptom · 01
App fails to start: Connection refused / timeout
Fix
Check database host, port, and network ACLs. Verify DataSource credentials. Test with a simple TCP connection using telnet or nc.
Symptom · 02
Queries return empty relations (null instead of data)
Fix
Ensure relations are explicitly loaded via 'relations' option or leftJoinAndSelect. TypeORM does not auto-join relations.
Symptom · 03
Slow queries even with indexes
Fix
Enable query logging: logging: ['query', 'error']. Analyze the SQL output in PostgreSQL slow query log or EXPLAIN ANALYZE.
Symptom · 04
TypeError: Cannot read properties of undefined (reading 'id')
Fix
Check that findOne returns null (not undefined) — use optional chaining or if (result) before accessing properties.
Symptom · 05
Memory leak over time
Fix
Ensure every QueryRunner is released after use. Use getRepository and QueryBuilder which auto-manage connections. Avoid creating DataSource instances per request.
★ Quick Debug Commands for TypeORMRun these commands when things go sideways.
Missing table or column after migration
Immediate action
Run migration:show to see pending migrations. Check if migration was applied.
Commands
npx typeorm migration:show -d path/to/data-source.ts
npx typeorm migration:run -d path/to/data-source.ts
Fix now
If migration failed silently, run manually: INSERT INTO migrations (timestamp, name) VALUES (...) to skip.
N+1 queries causing slow pages+
Immediate action
Enable logging to see number of SQL statements per page load.
Commands
Set logging: ['query'] in DataSource config.
Check repository.find({ relations: ['posts'] }) instead of lazy loading in templates.
Fix now
Add Eager loading or use QueryBuilder with leftJoinAndSelect.
FindOne returns undefined properties+
Immediate action
Check that entity properties are defined without optional (?) unless null is expected.
Commands
console.log(JSON.stringify(result)); // to see actual shape
Use TypeScript strict mode to catch missing returns.
Fix now
Replace findOne with findOneOrFail or add null check: const user = await repo.findOne(...); if (!user) throw new NotFoundException();
Active Record vs Data Mapper in TypeORM
AspectActive RecordData Mapper
PatternEntity extends BaseEntity and includes save/remove methodsSeparate Repository class handles persistence
Usageuser.save() directly on entity instanceuserRepo.save(user)
TestabilityHarder to mock — entity class has persistence logicEasier — Repository can be mocked
Typical use caseSimple apps, Rails-styleComplex apps, separation of concerns
TypeORM supportUse @Entity() with extends BaseEntityUse @Entity() without extending; use getRepository()
⚙ Quick Reference
11 commands from this guide
FileCommand / CodePurpose
data-source.tsexport const AppDataSource = new DataSource({Setting Up TypeORM
entitiesUser.ts@Entity('users')Defining Entities
serviceuser.service.tsconst userRepo = AppDataSource.getRepository(User);CRUD with Repository
relations-example.tsconst user = await userRepo.findOne({ where: { id: 1 } });Relations
migration-commands.shnpx typeorm migration:generate -d src/data-source.ts src/migrations/AddEmailVeri...Migrations
PrerequisitesCheck.sqlSELECTWhat You’d Better Know Before Touching This Shit (Prerequisi
OperatorsExample.sqlWHERE age BETWEEN :minAge AND :maxAgeOperators in TypeORM Query Builder
WorkingDirFix.sqlexport const AppDataSource = new DataSource({Working Directory Gotchas with TypeOrm
ImportExample.sqlEntity,Importing from TypeORM Correctly
migration-example.sqlCREATE TABLE "user" (TypeORM in 2026
production-migration.sqlALTER TABLE "user" ADD COLUMN "age" integer NOT NULL DEFAULT 0;TypeORM Migrations

Key takeaways

1
Never use synchronize
true in production — use TypeORM migrations instead.
2
findOne returns null (not undefined) when not found
check explicitly.
3
Relations must be explicitly loaded with relations option or joins
TypeORM is lazy by default.
4
QueryBuilder is for complex queries; the Repository API covers 80% of CRUD needs.
5
TypeORM supports both Active Record (entity extends BaseEntity) and Data Mapper (repository) patterns.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the difference between save() and update() in TypeORM?
Q02JUNIOR
Why is synchronize: true dangerous in production?
Q03JUNIOR
How do you load related entities in TypeORM?
Q04SENIOR
What is the N+1 problem in TypeORM and how do you solve it?
Q05SENIOR
How do you handle database migrations in TypeORM in a team environment?
Q01 of 05SENIOR

What is the difference between save() and update() in TypeORM?

ANSWER
save() is a full entity lifecycle method: it checks if the entity has an id, and either inserts or updates accordingly. It also triggers lifecycle hooks (@BeforeInsert, @AfterUpdate, etc.) and cascades. update() is a direct SQL UPDATE that only sets the provided columns — no lifecycle hooks, no cascade, and it skips validation. Use save() for normal CRUD where hooks matter; use update() for bulk updates where performance is critical.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between save() and update() in TypeORM?
02
Why should I never use synchronize: true in production?
03
How do I fix the 'No metadata for entity' error?
04
Can I use TypeORM with multiple databases?
05
What is the difference between find, findOne, and findOneOrFail?
N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's ORM. Mark it forged?

6 min read · try the examples if you haven't

Previous
Prisma ORM Basics
6 / 9 · ORM
Next
ActiveRecord vs DataMapper Pattern