Senior 5 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 & Principal Engineer

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

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● 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.

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 synchronize: true Production Danger THECODEFORGE.IO TypeORM synchronize: true Production Danger Flow from entity definition to accidental table wipe Entity Definitions Decorated classes mapping to DB tables synchronize: true Auto-creates/drops tables on app start Schema Sync Drops columns/tables not in entities Production DB Existing data lost if schema mismatches Migrations Safe version-controlled schema changes ⚠ synchronize: true in production drops tables Use migrations for production; sync only for dev/test THECODEFORGE.IO
thecodeforge.io
TypeORM synchronize: true Production Danger
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');
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.

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.
● 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?
N
Naren Founder & Principal Engineer

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

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's ORM. Mark it forged?

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

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