Typeorm
TypeORM for decorator-based entity modeling, migrations, and Active Record or Data Mapper patterns in TypeScript/Node.js
You are an expert in TypeORM for database access and data modeling. You help developers define entities, write queries, manage migrations, and apply TypeORM patterns effectively. ## Key Points - **Entities** — decorator-based class definitions mapping to database tables - **Repositories** — data access layer for each entity - **Query Builder** — fluent API for complex queries - **Migrations** — versioned schema changes - **DataSource** — connection configuration and management - **Never use `synchronize: true` in production** — it can drop columns and data. Always use migrations. - **Use the Query Builder** for complex queries with joins, subqueries, or aggregations; use repositories for simple CRUD. - **Always parameterize** query builder conditions (`:param` syntax) to prevent SQL injection. - **Prefer `find` options over Query Builder** for straightforward lookups — they are more type-safe and readable. - **Index foreign key columns** explicitly; TypeORM does not auto-create indexes on `@ManyToOne` columns. - **Use `select` in find options** to avoid loading unnecessary columns on large tables. - **Initialize the DataSource once** at application startup and reuse it throughout the app lifecycle. ## Quick Example ```bash npm install typeorm reflect-metadata npm install pg # or mysql2, better-sqlite3, etc. ``` ```bash npx typeorm migration:generate -d src/data-source.ts src/migrations/Init npx typeorm migration:run -d src/data-source.ts npx typeorm migration:revert -d src/data-source.ts ```
skilldb get orm-skills/TypeormFull skill: 247 linesTypeORM — Database Toolkit
You are an expert in TypeORM for database access and data modeling. You help developers define entities, write queries, manage migrations, and apply TypeORM patterns effectively.
Overview
TypeORM is a mature ORM for TypeScript and JavaScript that supports both Active Record and Data Mapper patterns. It uses decorators to define entities and provides a rich query builder. It supports PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, Oracle, and MongoDB.
Key components:
- Entities — decorator-based class definitions mapping to database tables
- Repositories — data access layer for each entity
- Query Builder — fluent API for complex queries
- Migrations — versioned schema changes
- DataSource — connection configuration and management
Setup & Configuration
Installation
npm install typeorm reflect-metadata
npm install pg # or mysql2, better-sqlite3, etc.
Enable in tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
DataSource Configuration (src/data-source.ts)
import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { User } from './entities/User';
import { Post } from './entities/Post';
export const AppDataSource = new DataSource({
type: 'postgres',
host: process.env.DB_HOST ?? 'localhost',
port: Number(process.env.DB_PORT ?? 5432),
username: process.env.DB_USER ?? 'postgres',
password: process.env.DB_PASSWORD ?? '',
database: process.env.DB_NAME ?? 'myapp',
synchronize: false, // never true in production
logging: process.env.NODE_ENV === 'development',
entities: [User, Post],
migrations: ['src/migrations/*.ts'],
});
Entity Definitions
// src/entities/User.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
import { Post } from './Post';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Index({ unique: true })
@Column({ type: 'varchar', length: 255 })
email: string;
@Column({ type: 'varchar', length: 100, nullable: true })
name: string | null;
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// src/entities/Post.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Index, CreateDateColumn } from 'typeorm';
import { User } from './User';
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'varchar', length: 255 })
title: string;
@Column({ type: 'text', nullable: true })
content: string | null;
@Column({ type: 'boolean', default: false })
published: boolean;
@Index()
@Column()
authorId: number;
@ManyToOne(() => User, (user) => user.posts)
@JoinColumn({ name: 'authorId' })
author: User;
@CreateDateColumn()
createdAt: Date;
}
Migrations
npx typeorm migration:generate -d src/data-source.ts src/migrations/Init
npx typeorm migration:run -d src/data-source.ts
npx typeorm migration:revert -d src/data-source.ts
Core Patterns
Repository Pattern (Data Mapper)
import { AppDataSource } from './data-source';
import { User } from './entities/User';
const userRepo = AppDataSource.getRepository(User);
// Create
const user = userRepo.create({ email: 'alice@example.com', name: 'Alice' });
await userRepo.save(user);
// Find
const found = await userRepo.findOne({
where: { email: 'alice@example.com' },
relations: { posts: true },
});
// Find with conditions
const users = await userRepo.find({
where: { name: Like('%Ali%') },
order: { createdAt: 'DESC' },
skip: 0,
take: 10,
relations: ['posts'],
});
// Update
await userRepo.update({ id: 1 }, { name: 'Alice Updated' });
// Upsert
await userRepo.upsert(
{ email: 'alice@example.com', name: 'Alice' },
{ conflictPaths: ['email'] },
);
// Delete
await userRepo.delete({ id: 1 });
// Soft delete (requires @DeleteDateColumn)
await userRepo.softDelete({ id: 1 });
Query Builder
const postsWithAuthors = await AppDataSource.getRepository(Post)
.createQueryBuilder('post')
.innerJoinAndSelect('post.author', 'user')
.where('post.published = :published', { published: true })
.andWhere('user.email LIKE :domain', { domain: '%@example.com' })
.orderBy('post.createdAt', 'DESC')
.skip(0)
.take(20)
.getMany();
// Aggregation
const stats = await AppDataSource.getRepository(Post)
.createQueryBuilder('post')
.select('post.authorId', 'authorId')
.addSelect('COUNT(*)', 'postCount')
.groupBy('post.authorId')
.having('COUNT(*) > :min', { min: 5 })
.getRawMany();
Transactions
await AppDataSource.transaction(async (manager) => {
const user = manager.create(User, { email: 'bob@example.com', name: 'Bob' });
await manager.save(user);
const post = manager.create(Post, { title: 'Hello', authorId: user.id, published: true });
await manager.save(post);
});
Core Philosophy
TypeORM brings the decorator-based entity modeling pattern from Java's Hibernate and .NET's Entity Framework to the TypeScript ecosystem. Entities are classes decorated with @Entity, @Column, @ManyToOne, and other metadata that TypeORM uses to generate SQL, build queries, and manage relationships. This approach is familiar to developers from enterprise ORM backgrounds and integrates naturally with TypeScript's type system, though it requires the experimentalDecorators and emitDecoratorMetadata compiler options.
TypeORM supports both the Active Record and Data Mapper patterns, and choosing the right one shapes the entire application architecture. Active Record puts query methods on the entity itself (User.find(), user.save()), which is simple but couples your domain objects to the database. Data Mapper separates entities (plain data classes) from repositories (query objects), which is more complex but keeps domain logic testable and database-independent. For applications that will grow beyond a few models, the Data Mapper pattern with explicit repositories is the more sustainable choice.
The Query Builder is TypeORM's most powerful feature and the escape hatch for anything the repository API cannot express. It provides a fluent, chainable API for joins, subqueries, aggregations, and raw SQL fragments, all with parameterized bindings. The key insight is when to use it: simple CRUD operations are clearer and more type-safe with the repository's find and save methods; complex queries with multiple joins, aggregations, or conditional logic are better expressed with the Query Builder. Using the Query Builder for simple lookups adds verbosity without benefit; using repository methods for complex queries forces awkward workarounds.
Anti-Patterns
-
Using
synchronize: truebeyond initial prototyping — auto-synchronization compares entities to the database and applies changes directly, which can drop columns, lose data, and create schema inconsistencies. Always use the migration system for any environment where data matters. -
Eagerly loading all relations by default — setting
eager: trueon entity relations or always including all relations in find options loads far more data than needed. This creates performance problems that worsen as the data grows. Be explicit about which relations each query needs. -
Saving plain objects expecting an update — calling
repository.save()with a plain JavaScript object (not an entity loaded from the database or created withrepository.create()) can trigger an INSERT instead of an UPDATE, creating duplicate records. -
Serializing entities with bidirectional relations — entities with circular references (
User.posts -> Post.author -> User.posts) cause infinite loops inJSON.stringify. Useclass-transformerwith@Excludedecorators or manual DTO mapping to control serialization. -
Querying before DataSource initialization completes — calling
getRepository()or running queries beforeAppDataSource.initialize()resolves throws runtime errors. Ensure initialization completes (and error handling is in place) before the application starts accepting requests.
Best Practices
- Never use
synchronize: truein production — it can drop columns and data. Always use migrations. - Use the Query Builder for complex queries with joins, subqueries, or aggregations; use repositories for simple CRUD.
- Always parameterize query builder conditions (
:paramsyntax) to prevent SQL injection. - Prefer
findoptions over Query Builder for straightforward lookups — they are more type-safe and readable. - Index foreign key columns explicitly; TypeORM does not auto-create indexes on
@ManyToOnecolumns. - Use
selectin find options to avoid loading unnecessary columns on large tables. - Initialize the DataSource once at application startup and reuse it throughout the app lifecycle.
Common Pitfalls
- Eager loading everything: Using
eager: trueon relations or always includingrelationsloads excessive data. Be explicit about what you need. - Circular relation serialization: Entities with bidirectional relations cause infinite loops in
JSON.stringify. Useclass-transformeror manual serialization. synchronize: truedata loss: Auto-sync can drop columns when you rename fields. It is only safe for rapid prototyping.- Decorator metadata missing: Forgetting
reflect-metadataimport oremitDecoratorMetadatain tsconfig causes silent failures. - Saving detached entities: Calling
save()on a plain object (not created viacreate()or loaded from DB) can cause unexpected inserts instead of updates. - Connection not initialized: Querying before
AppDataSource.initialize()resolves throws runtime errors. Ensure initialization completes before handling requests.
Install this skill directly: skilldb add orm-skills
Related Skills
Drizzle
Drizzle ORM for lightweight, type-safe SQL in TypeScript with zero dependencies, SQL-like syntax, and schema-as-code
Knex
Knex.js SQL query builder for Node.js with schema building, migrations, seed files, and support for PostgreSQL, MySQL, and SQLite
Kysely
Kysely type-safe SQL query builder for TypeScript with zero overhead, full autocompletion, and no code generation
Mikro ORM
MikroORM for TypeScript with Unit of Work, Identity Map, decorator-based entities, and support for SQL and MongoDB
Mongoose
Mongoose ODM for MongoDB with schema validation, middleware hooks, population, virtuals, and TypeScript support in Node.js
Prisma
Prisma ORM for type-safe database access with auto-generated client, migrations, and schema-first modeling in TypeScript/Node.js