Senior 3 min · March 17, 2026

TypeORM — synchronize: true Wiped a Production Table

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

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
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
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.

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.

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');
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[];
}
Entity = DTO + Schema definition
  • 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.

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();
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
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
Migrations = Version control for your database
  • 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.
● 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()

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.

Common mistakes to avoid

5 patterns
×

Using synchronize: true in production

Symptom
Tables get dropped or columns deleted unexpectedly upon deployment. Data loss occurs silently.
Fix
Set synchronize: false in all DataSource configs. Generate and run migrations instead.
×

Not loading relations explicitly

Symptom
Properties like user.posts are undefined even though the relation exists. Code crashes with TypeError when accessing nested data.
Fix
Always include relations: { ... } in find/findOne calls, or use leftJoinAndSelect in QueryBuilder.
×

Creating DataSource per request (especially in serverless)

Symptom
Connection pool exhaustion, slow cold starts, or connection limit errors from the database.
Fix
Create a single global DataSource instance and reuse it across requests. In serverless, cache it outside the handler.
×

Assuming save() always performs INSERT

Symptom
When an entity has an id field set, save() performs an UPDATE instead of INSERT, potentially overwriting existing data.
Fix
Use insert() for new records if you want to guarantee INSERT. Or ensure id is undefined/omitted when creating new entities.
×

Ignoring the N+1 query problem

Symptom
Page load times increase linearly with number of parent records. Database gets hammered with hundreds of small queries.
Fix
Use leftJoinAndSelect or relations option to eager load needed relations in a single query. Monitor query logs regularly.
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?
🔥

That's ORM. Mark it forged?

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

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