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: '[email protected]',
name: 'Alice',
});
await userRepository.save(user);
// Find all
const users = await userRepository.find();
// Find one
const user = await userRepository.findOne({
where: { email: '[email protected]' }
});
// 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: '[email protected]',
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('[email protected]');
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:
- Use entities with decorators
- Leverage repositories
- Migrations for production
- Relations for complex data
- Query builder for flexibility
Next Steps:
- Compare Prisma
- Learn PostgreSQL
- Build Express API
Resources: