본문으로 건너뛰기 TypeORM Complete Guide | TypeScript ORM for Node.js

TypeORM Complete Guide | TypeScript ORM for Node.js

TypeORM Complete Guide | TypeScript ORM for Node.js

이 글의 핵심

TypeORM is a TypeScript-first ORM for Node.js that works with PostgreSQL, MySQL, SQLite, and more. It provides decorators, migrations, relations, and full type safety.

Introduction

TypeORM is an ORM (Object-Relational Mapping) library that brings SQL databases to TypeScript and Node.js with decorators, repositories, migrations, and full type safety.

Why TypeORM?

Raw SQL:

const result = await db.query('SELECT * FROM users WHERE email = $1', [email]);
const user = result.rows[0]; // No types

With TypeORM:

@Entity()
class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  email: string;
}

const user = await userRepository.findOne({ where: { email } }); // Fully typed!

1. Installation

npm install typeorm reflect-metadata
npm install -D @types/node

# Database driver (choose one)
npm install pg          # PostgreSQL
npm install mysql2      # MySQL
npm install sqlite3     # SQLite

2. Configuration

// src/data-source.ts
import { DataSource } from 'typeorm';
import { User } from './entities/User';

export const AppDataSource = new DataSource({
  type: 'postgres',
  host: 'localhost',
  port: 5432,
  username: 'postgres',
  password: 'password',
  database: 'myapp',
  entities: [User],
  synchronize: true, // Auto-create tables (dev only!)
  logging: true,
});

// Initialize
await AppDataSource.initialize();

3. Entity Definition

import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity('users') // Table name
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  email: string;

  @Column()
  name: string;

  @Column({ nullable: true })
  bio: string | null;

  @Column({ type: 'int', default: 0 })
  age: number;

  @Column({ type: 'varchar', length: 50 })
  username: string;

  @Column({ default: true })
  isActive: boolean;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

4. Repository Pattern

import { AppDataSource } from './data-source';
import { User } from './entities/User';

const userRepository = AppDataSource.getRepository(User);

// Create
const user = userRepository.create({
  email: 'alice@example.com',
  name: 'Alice',
});
await userRepository.save(user);

// Find all
const users = await userRepository.find();

// Find one
const user = await userRepository.findOne({
  where: { email: 'alice@example.com' }
});

// Find with conditions
const activeUsers = await userRepository.find({
  where: { isActive: true }
});

// Update
await userRepository.update(
  { id: 1 },
  { name: 'Alice Updated' }
);

// Delete
await userRepository.delete({ id: 1 });

// Count
const count = await userRepository.count();

5. Query Builder

// Complex query
const users = await userRepository
  .createQueryBuilder('user')
  .where('user.age > :age', { age: 18 })
  .andWhere('user.isActive = :isActive', { isActive: true })
  .orderBy('user.createdAt', 'DESC')
  .limit(10)
  .getMany();

// With relations
const users = await userRepository
  .createQueryBuilder('user')
  .leftJoinAndSelect('user.posts', 'post')
  .where('post.published = :published', { published: true })
  .getMany();

// Aggregate
const result = await userRepository
  .createQueryBuilder('user')
  .select('COUNT(*)', 'count')
  .where('user.isActive = :isActive', { isActive: true })
  .getRawOne();

6. Relations

One-to-Many

import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from 'typeorm';

@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)
  author: User;
}

// Usage
const user = await userRepository.findOne({
  where: { id: 1 },
  relations: ['posts'], // Load posts
});

console.log(user.posts); // Post[]

Many-to-Many

@Entity()
export class Post {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @ManyToMany(() => Tag, (tag) => tag.posts)
  @JoinTable() // Creates join table
  tags: Tag[];
}

@Entity()
export class Tag {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @ManyToMany(() => Post, (post) => post.tags)
  posts: Post[];
}

// Usage
const post = await postRepository.findOne({
  where: { id: 1 },
  relations: ['tags'],
});

console.log(post.tags); // Tag[]

One-to-One

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @OneToOne(() => Profile, (profile) => profile.user)
  @JoinColumn()
  profile: Profile;
}

@Entity()
export class Profile {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  bio: string;

  @OneToOne(() => User, (user) => user.profile)
  user: User;
}

7. Migrations

Generate Migration

# Auto-generate from entity changes
npx typeorm migration:generate -d src/data-source.ts -n CreateUserTable

# Create empty migration
npx typeorm migration:create src/migrations/CreateUserTable

Migration File

// src/migrations/1234567890-CreateUserTable.ts
import { MigrationInterface, QueryRunner } from 'typeorm';

export class CreateUserTable1234567890 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      CREATE TABLE "users" (
        "id" SERIAL PRIMARY KEY,
        "email" VARCHAR UNIQUE NOT NULL,
        "name" VARCHAR NOT NULL,
        "createdAt" TIMESTAMP DEFAULT NOW()
      )
    `);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP TABLE "users"`);
  }
}

Run Migrations

# Run migrations
npx typeorm migration:run -d src/data-source.ts

# Revert last migration
npx typeorm migration:revert -d src/data-source.ts

# Show migration status
npx typeorm migration:show -d src/data-source.ts

8. Transactions

await AppDataSource.transaction(async (manager) => {
  // Create user
  const user = manager.create(User, {
    email: 'alice@example.com',
    name: 'Alice',
  });
  await manager.save(user);

  // Create post
  const post = manager.create(Post, {
    title: 'My First Post',
    author: user,
  });
  await manager.save(post);

  // Both saved or both rolled back
});

9. Validation

npm install class-validator
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { IsEmail, MinLength, MaxLength, Min, Max } from 'class-validator';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  @IsEmail()
  email: string;

  @Column()
  @MinLength(2)
  @MaxLength(50)
  name: string;

  @Column()
  @Min(0)
  @Max(120)
  age: number;
}

// Validate before save
import { validate } from 'class-validator';

const user = new User();
user.email = 'invalid-email';

const errors = await validate(user);
if (errors.length > 0) {
  console.log('Validation failed:', errors);
}

10. Custom Repository

import { EntityRepository, Repository } from 'typeorm';

@EntityRepository(User)
export class UserRepository extends Repository<User> {
  async findByEmail(email: string): Promise<User | null> {
    return this.findOne({ where: { email } });
  }

  async findActiveUsers(): Promise<User[]> {
    return this.find({ where: { isActive: true } });
  }

  async findWithPosts(userId: number): Promise<User | null> {
    return this.findOne({
      where: { id: userId },
      relations: ['posts'],
    });
  }
}

// Usage
const userRepository = AppDataSource.getCustomRepository(UserRepository);
const user = await userRepository.findByEmail('alice@example.com');

11. Subscribers (Hooks)

import { EntitySubscriberInterface, EventSubscriber, InsertEvent, UpdateEvent } from 'typeorm';
import { User } from './entities/User';

@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
  listenTo() {
    return User;
  }

  beforeInsert(event: InsertEvent<User>) {
    console.log('Before user insert:', event.entity);
  }

  afterInsert(event: InsertEvent<User>) {
    console.log('After user insert:', event.entity);
  }

  beforeUpdate(event: UpdateEvent<User>) {
    console.log('Before user update:', event.entity);
  }

  afterUpdate(event: UpdateEvent<User>) {
    console.log('After user update:', event.entity);
  }
}

12. Real-World Example: Blog API

// entities/User.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn } from 'typeorm';
import { Post } from './Post';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  email: string;

  @Column()
  username: string;

  @Column()
  password: string;

  @Column({ default: 'user' })
  role: 'user' | 'admin';

  @OneToMany(() => Post, (post) => post.author)
  posts: Post[];

  @CreateDateColumn()
  createdAt: Date;
}

// entities/Post.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, ManyToMany, JoinTable, CreateDateColumn } from 'typeorm';
import { User } from './User';
import { Tag } from './Tag';

@Entity('posts')
export class Post {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column({ unique: true })
  slug: string;

  @Column('text')
  content: string;

  @Column({ default: false })
  published: boolean;

  @ManyToOne(() => User, (user) => user.posts)
  author: User;

  @ManyToMany(() => Tag, (tag) => tag.posts)
  @JoinTable()
  tags: Tag[];

  @CreateDateColumn()
  createdAt: Date;
}

// entities/Tag.ts
@Entity('tags')
export class Tag {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  name: string;

  @ManyToMany(() => Post, (post) => post.tags)
  posts: Post[];
}

// Express API
import express from 'express';
import { AppDataSource } from './data-source';
import { Post } from './entities/Post';
import { User } from './entities/User';

const app = express();
app.use(express.json());

const postRepository = AppDataSource.getRepository(Post);
const userRepository = AppDataSource.getRepository(User);

// Get all posts
app.get('/api/posts', async (req, res) => {
  try {
    const posts = await postRepository.find({
      where: { published: true },
      relations: ['author', 'tags'],
      order: { createdAt: 'DESC' },
    });

    res.json({ posts });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Get single post
app.get('/api/posts/:slug', async (req, res) => {
  try {
    const post = await postRepository.findOne({
      where: { slug: req.params.slug },
      relations: ['author', 'tags'],
    });

    if (!post) {
      return res.status(404).json({ error: 'Post not found' });
    }

    res.json({ post });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Create post
app.post('/api/posts', authenticate, async (req, res) => {
  try {
    const { title, content, tagNames } = req.body;

    const author = await userRepository.findOne({ where: { id: req.user.id } });

    const post = postRepository.create({
      title,
      slug: title.toLowerCase().replace(/\s+/g, '-'),
      content,
      author,
    });

    await postRepository.save(post);

    res.status(201).json({ post });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// Update post
app.put('/api/posts/:id', authenticate, async (req, res) => {
  try {
    const post = await postRepository.findOne({
      where: { id: parseInt(req.params.id) },
      relations: ['author'],
    });

    if (!post) {
      return res.status(404).json({ error: 'Post not found' });
    }

    if (post.author.id !== req.user.id) {
      return res.status(403).json({ error: 'Unauthorized' });
    }

    Object.assign(post, req.body);
    await postRepository.save(post);

    res.json({ post });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

await AppDataSource.initialize();
app.listen(3000);

13. Soft Deletes

import { Entity, Column, DeleteDateColumn } from 'typeorm';

@Entity()
export class User {
  @Column()
  name: string;

  @DeleteDateColumn()
  deletedAt?: Date; // Soft delete timestamp
}

// Soft delete
await userRepository.softDelete({ id: 1 });

// Find with deleted
const users = await userRepository.find({ withDeleted: true });

// Restore
await userRepository.restore({ id: 1 });

// Hard delete
await userRepository.delete({ id: 1 });

14. Indexing

import { Entity, Column, Index } from 'typeorm';

@Entity()
@Index(['email', 'username']) // Compound index
export class User {
  @Column()
  @Index() // Single column index
  email: string;

  @Column()
  @Index({ unique: true }) // Unique index
  username: string;

  @Column()
  @Index('idx_name') // Named index
  name: string;
}

15. Best Practices

1. Use Repositories

// Good: use repository
const userRepository = AppDataSource.getRepository(User);
const users = await userRepository.find();

// Bad: direct query
await AppDataSource.query('SELECT * FROM users');

2. Disable synchronize in Production

export const AppDataSource = new DataSource({
  synchronize: process.env.NODE_ENV === 'development', // Only in dev
  migrations: ['src/migrations/*.ts'],
});

3. Use Migrations

# Generate migration from entity changes
npx typeorm migration:generate -d src/data-source.ts -n AddUserRole

# Run in production
npx typeorm migration:run -d dist/data-source.js

Summary

TypeORM brings type-safe SQL to Node.js:

  • Decorators for entity definition
  • Repository pattern for clean queries
  • Migrations for schema management
  • Relations (1-1, 1-N, N-N)
  • TypeScript-first with full inference

Key Takeaways:

  1. Use entities with decorators
  2. Leverage repositories
  3. Migrations for production
  4. Relations for complex data
  5. Query builder for flexibility

Next Steps:

Resources:


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. TypeORM is a TypeScript-first ORM for Node.js that works with PostgreSQL, MySQL, SQLite, and more. It provides decorators, migrations, relations, and full type safety.

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers TypeORM, ORM, TypeScript, Database, PostgreSQL, MySQL.