TypeORM — synchronize: true Wiped a Production Table
TypeORM's synchronize: true silently dropped a 'created_at' column in production, wiping timestamps.
20+ years shipping high-throughput database systems. Lessons pulled from things that broke in production.
- 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
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.
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.
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.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.
- 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.
Column() with a default value does not automatically backfill existing rows.PrimaryGeneratedColumn()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.
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.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.
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.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.
- 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.
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.
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. Iftypeofvsinstanceofconfuses 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.
.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.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.
:param syntax to avoid SQL injection—TypeORM will escape values.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.
__dirname or set NODE_PATH explicitly in all launch scripts.__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'.
EntityRepository or getRepository (deprecated in 0.3) will fail silently or throw runtime errors. Always use dataSource.getRepository().dataSource.getRepository() for runtime repository access.Synchronize: True Wiped a Production Table
- 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.
npx typeorm migration:show -d path/to/data-source.tsnpx typeorm migration:run -d path/to/data-source.tsKey takeaways
Common mistakes to avoid
5 patternsUsing synchronize: true in production
Not loading relations explicitly
Creating DataSource per request (especially in serverless)
Assuming save() always performs INSERT
save() performs an UPDATE instead of INSERT, potentially overwriting existing data.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
Interview Questions on This Topic
What is the difference between save() and update() in TypeORM?
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.Frequently Asked Questions
20+ years shipping high-throughput database systems. Lessons pulled from things that broke in production.
That's ORM. Mark it forged?
5 min read · try the examples if you haven't