--- name: typeorm description: Guidelines for developing with TypeORM, a full-featured ORM for TypeScript and JavaScript supporting multiple databases --- # TypeORM Development Guidelines You are an expert in TypeORM, TypeScript, and database design with a focus on the Data Mapper pattern and enterprise application architecture. ## Core Principles - TypeORM supports both Active Record and Data Mapper patterns - Uses TypeScript decorators for entity and column definitions - Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, and more - Works in Node.js, Browser, Ionic, Cordova, React Native, NativeScript, Expo, and Electron - First-class support for database migrations ## TypeScript Configuration Required settings in tsconfig.json: ```json { "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true, "strict": true, "target": "ES2020", "module": "commonjs", "moduleResolution": "node" } } ``` ## Entity Definition ### Basic Entity ```typescript import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; @Entity('users') export class User { @PrimaryGeneratedColumn() id: number; @Column({ type: 'varchar', length: 255, unique: true }) email: string; @Column({ type: 'varchar', length: 255, nullable: true }) name: string | null; @Column({ type: 'boolean', default: true }) isActive: boolean; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; } ``` ### Primary Key Options ```typescript // Auto-increment @PrimaryGeneratedColumn() id: number; // UUID @PrimaryGeneratedColumn("uuid") id: string; // Custom primary key @PrimaryColumn() id: string; // Composite primary key @Entity() export class OrderItem { @PrimaryColumn() orderId: number; @PrimaryColumn() productId: number; } ``` ### Column Decorators ```typescript @Entity() export class Product { @PrimaryGeneratedColumn() id: number; // String columns @Column({ type: 'varchar', length: 255 }) name: string; @Column({ type: 'text', nullable: true }) description: string | null; // Numeric columns @Column({ type: 'decimal', precision: 10, scale: 2 }) price: number; @Column({ type: 'int', default: 0 }) stock: number; // Boolean @Column({ type: 'boolean', default: true }) isAvailable: boolean; // JSON @Column({ type: 'jsonb', nullable: true }) metadata: Record | null; // Enum @Column({ type: 'enum', enum: ['active', 'inactive', 'pending'], default: 'pending', }) status: 'active' | 'inactive' | 'pending'; // Timestamps @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; @DeleteDateColumn() deletedAt: Date | null; // For soft deletes // Version column for optimistic locking @VersionColumn() version: number; } ``` ## Relationships ### One-to-One ```typescript @Entity() export class User { @PrimaryGeneratedColumn() id: number; @OneToOne(() => Profile, profile => profile.user, { cascade: true }) @JoinColumn() profile: Profile; } @Entity() export class Profile { @PrimaryGeneratedColumn() id: number; @Column() bio: string; @OneToOne(() => User, user => user.profile) user: User; } ``` ### One-to-Many / Many-to-One ```typescript @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @OneToMany(() => Post, post => post.author) posts: Post[]; } @Entity() export class Post { @PrimaryGeneratedColumn() id: number; @Column() title: string; @ManyToOne(() => User, user => user.posts, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'author_id' }) author: User; @Column() authorId: number; // Explicit foreign key column } ``` ### Many-to-Many ```typescript @Entity() export class Post { @PrimaryGeneratedColumn() id: number; @Column() title: string; @ManyToMany(() => Tag, tag => tag.posts) @JoinTable({ name: 'post_tags', joinColumn: { name: 'post_id' }, inverseJoinColumn: { name: 'tag_id' }, }) tags: Tag[]; } @Entity() export class Tag { @PrimaryGeneratedColumn() id: number; @Column({ unique: true }) name: string; @ManyToMany(() => Post, post => post.tags) posts: Post[]; } ``` ## Repository Pattern ### Basic Repository Usage ```typescript import { AppDataSource } from './data-source'; import { User } from './entities/User'; const userRepository = AppDataSource.getRepository(User); // Find all const users = await userRepository.find(); // Find with conditions const activeUsers = await userRepository.find({ where: { isActive: true }, }); // Find one const user = await userRepository.findOne({ where: { id: 1 }, }); // Find or fail const user = await userRepository.findOneOrFail({ where: { id: 1 }, }); // Save const newUser = userRepository.create({ email: 'user@example.com', name: 'John Doe', }); await userRepository.save(newUser); // Update await userRepository.update({ id: 1 }, { name: 'Jane Doe' }); // Delete await userRepository.delete({ id: 1 }); // Soft delete (requires @DeleteDateColumn) await userRepository.softDelete({ id: 1 }); ``` ### Custom Repository ```typescript import { Repository, DataSource } from 'typeorm'; import { User } from './entities/User'; export class UserRepository extends Repository { constructor(private dataSource: DataSource) { super(User, dataSource.createEntityManager()); } async findByEmail(email: string): Promise { return this.findOne({ where: { email } }); } async findActiveUsers(): Promise { return this.find({ where: { isActive: true }, order: { createdAt: 'DESC' }, }); } async findWithPosts(userId: number): Promise { return this.findOne({ where: { id: userId }, relations: ['posts'], }); } } ``` ### Query Builder ```typescript const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .where('user.isActive = :isActive', { isActive: true }) .andWhere('post.publishedAt IS NOT NULL') .orderBy('user.createdAt', 'DESC') .skip(0) .take(10) .getMany(); // With raw results const result = await userRepository .createQueryBuilder('user') .select('COUNT(*)', 'count') .where('user.isActive = :isActive', { isActive: true }) .getRawOne(); // Insert with query builder await userRepository .createQueryBuilder() .insert() .into(User) .values([ { email: 'user1@example.com', name: 'User 1' }, { email: 'user2@example.com', name: 'User 2' }, ]) .execute(); ``` ## Data Source Configuration ```typescript // data-source.ts 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: parseInt(process.env.DB_PORT || '5432'), username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, // Entity configuration entities: [User, Post], // Or use glob pattern: entities: ["src/entities/**/*.ts"] // Migrations migrations: ['src/migrations/**/*.ts'], // Synchronize - NEVER use in production synchronize: false, // Logging logging: process.env.NODE_ENV === 'development', // Connection pool poolSize: 10, // SSL (for production) ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, }); // Initialize connection AppDataSource.initialize() .then(() => console.log('Data Source initialized')) .catch(error => console.error('Error initializing Data Source:', error)); ``` ## Migrations ### Creating Migrations ```bash # Generate migration from entity changes npx typeorm migration:generate src/migrations/CreateUsers -d src/data-source.ts # Create empty migration npx typeorm migration:create src/migrations/SeedUsers # Run migrations npx typeorm migration:run -d src/data-source.ts # Revert last migration npx typeorm migration:revert -d src/data-source.ts ``` ### Migration File Structure ```typescript import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'; export class CreateUsers1234567890 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.createTable( new Table({ name: 'users', columns: [ { name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment', }, { name: 'email', type: 'varchar', length: '255', isUnique: true, }, { name: 'name', type: 'varchar', length: '255', isNullable: true, }, { name: 'is_active', type: 'boolean', default: true, }, { name: 'created_at', type: 'timestamp', default: 'CURRENT_TIMESTAMP', }, { name: 'updated_at', type: 'timestamp', default: 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP', }, ], }), true, ); await queryRunner.createIndex( 'users', new TableIndex({ name: 'IDX_USERS_EMAIL', columnNames: ['email'], }), ); } public async down(queryRunner: QueryRunner): Promise { await queryRunner.dropIndex('users', 'IDX_USERS_EMAIL'); await queryRunner.dropTable('users'); } } ``` ## Transactions ```typescript // Using QueryRunner const queryRunner = AppDataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const user = queryRunner.manager.create(User, { email: 'user@example.com', name: 'User', }); await queryRunner.manager.save(user); const post = queryRunner.manager.create(Post, { title: 'First Post', author: user, }); await queryRunner.manager.save(post); await queryRunner.commitTransaction(); } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } // Using transaction method await AppDataSource.transaction(async manager => { const user = manager.create(User, { email: 'user@example.com', name: 'User', }); await manager.save(user); const post = manager.create(Post, { title: 'First Post', author: user, }); await manager.save(post); }); ``` ## NestJS Integration ```typescript // app.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from './entities/user.entity'; import { UsersModule } from './users/users.module'; @Module({ imports: [ TypeOrmModule.forRoot({ type: 'postgres', host: 'localhost', port: 5432, username: 'user', password: 'password', database: 'db', entities: [User], synchronize: false, }), UsersModule, ], }) export class AppModule {} // users/users.module.ts @Module({ imports: [TypeOrmModule.forFeature([User])], providers: [UsersService], controllers: [UsersController], }) export class UsersModule {} // users/users.service.ts @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository, ) {} findAll(): Promise { return this.usersRepository.find(); } findOne(id: number): Promise { return this.usersRepository.findOneBy({ id }); } } ``` ## Best Practices ### Use Migrations in Production Never use `synchronize: true` in production. Always use migrations: ```typescript // Development: Use migrations, not sync synchronize: false, ``` ### Eager vs Lazy Loading ```typescript // Eager loading - loads relations automatically @OneToMany(() => Post, (post) => post.author, { eager: true }) posts: Post[]; // Lazy loading - loads relations on access @OneToMany(() => Post, (post) => post.author) posts: Promise; // Explicit loading (recommended) const user = await userRepository.findOne({ where: { id: 1 }, relations: ["posts"], }); ``` ### Avoid N+1 Queries ```typescript // Bad: N+1 queries const users = await userRepository.find(); for (const user of users) { console.log(user.posts); // Separate query for each user } // Good: Eager load relations const users = await userRepository.find({ relations: ['posts'], }); ``` ### Use Indexes ```typescript @Entity() @Index(['email']) @Index(['firstName', 'lastName']) export class User { @Column() @Index() email: string; @Column() firstName: string; @Column() lastName: string; } ``` ### Cascade Operations ```typescript @OneToMany(() => Post, (post) => post.author, { cascade: true, // Saves/removes related posts onDelete: "CASCADE", // Database-level cascade }) posts: Post[]; ``` ### Naming Strategies For consistent naming between TypeScript and database: ```typescript import { DefaultNamingStrategy, NamingStrategyInterface } from "typeorm"; import { snakeCase } from "typeorm/util/StringUtils"; export class SnakeNamingStrategy extends DefaultNamingStrategy implements NamingStrategyInterface { tableName(targetName: string, userSpecifiedName: string | undefined): string { return userSpecifiedName ? userSpecifiedName : snakeCase(targetName); } columnName(propertyName: string, customName: string, embeddedPrefixes: string[]): string { return snakeCase(embeddedPrefixes.join("_")) + (customName ? customName : snakeCase(propertyName)); } } // Use in data source config namingStrategy: new SnakeNamingStrategy(), ```