Skip to main content
Technology & EngineeringOrm247 lines

Typeorm

TypeORM for decorator-based entity modeling, migrations, and Active Record or Data Mapper patterns in TypeScript/Node.js

Quick Summary31 lines
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 lines
Paste into your CLAUDE.md or agent config

TypeORM — 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: true beyond 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: true on 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 with repository.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 in JSON.stringify. Use class-transformer with @Exclude decorators or manual DTO mapping to control serialization.

  • Querying before DataSource initialization completes — calling getRepository() or running queries before AppDataSource.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: 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.

Common Pitfalls

  • Eager loading everything: Using eager: true on relations or always including relations loads excessive data. Be explicit about what you need.
  • Circular relation serialization: Entities with bidirectional relations cause infinite loops in JSON.stringify. Use class-transformer or manual serialization.
  • synchronize: true data loss: Auto-sync can drop columns when you rename fields. It is only safe for rapid prototyping.
  • Decorator metadata missing: Forgetting reflect-metadata import or emitDecoratorMetadata in tsconfig causes silent failures.
  • Saving detached entities: Calling save() on a plain object (not created via create() 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

Get CLI access →