Setting Up TypeORM for Web Application
TypeORM is one of the oldest ORMs for TypeScript/Node.js. Supports Active Record and Data Mapper patterns, decorators for entity definition and rich feature set: migrations, subscribers, relations, query builder. Widely used in NestJS projects — de-facto standard there.
Installation
npm install typeorm reflect-metadata
npm install pg # PostgreSQL
# or mysql2, better-sqlite3, mongodb
# In tsconfig.json
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
}
Database connection
// db/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',
url: process.env.DATABASE_URL,
entities: [User, Post],
migrations: ['dist/db/migrations/*.js'],
migrationsTableName: 'migrations',
synchronize: false, // NEVER true in production
logging: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
extra: {
max: 20,
idleTimeoutMillis: 30000,
}
})
// Initialize
await AppDataSource.initialize()
Entities
// db/entities/User.ts
import {
Entity, PrimaryGeneratedColumn, Column, CreateDateColumn,
UpdateDateColumn, OneToMany, Index, BeforeInsert, BeforeUpdate
} from 'typeorm'
import bcrypt from 'bcrypt'
import { Post } from './Post'
export enum UserRole {
USER = 'user',
MODERATOR = 'moderator',
ADMIN = 'admin',
}
@Entity('users')
@Index(['email'])
export class User {
@PrimaryGeneratedColumn('uuid')
id: string
@Column({ length: 255, unique: true })
email: string
@Column({ length: 255 })
name: string
@Column({ select: false }) // don't include in SELECT by default
passwordHash: string
@Column({ type: 'enum', enum: UserRole, default: UserRole.USER })
role: UserRole
@OneToMany(() => Post, (post) => post.author)
posts: Post[]
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date
@BeforeInsert()
@BeforeUpdate()
async hashPassword() {
if (this.passwordHash && !this.passwordHash.startsWith('$2b$')) {
this.passwordHash = await bcrypt.hash(this.passwordHash, 12)
}
}
}
// db/entities/Post.ts
import {
Entity, PrimaryGeneratedColumn, Column, CreateDateColumn,
UpdateDateColumn, ManyToOne, ManyToMany, JoinTable, JoinColumn, Index
} from 'typeorm'
import { User } from './User'
import { Tag } from './Tag'
@Entity('posts')
@Index(['authorId', 'createdAt'])
@Index(['published', 'createdAt'])
export class Post {
@PrimaryGeneratedColumn('uuid')
id: string
@Column({ type: 'text' })
title: string
@Column({ type: 'text', nullable: true })
content: string | null
@Column({ default: false })
published: boolean
@Column({ name: 'author_id' })
authorId: string
@ManyToOne(() => User, (user) => user.posts, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'author_id' })
author: User
@ManyToMany(() => Tag, (tag) => tag.posts, { cascade: true })
@JoinTable({
name: 'posts_to_tags',
joinColumn: { name: 'post_id' },
inverseJoinColumn: { name: 'tag_id' }
})
tags: Tag[]
@Column({ default: 0 })
viewCount: number
@Column({ type: 'timestamptz', nullable: true })
publishedAt: Date | null
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date
}
Migrations
# Generate migration from diff
npx typeorm migration:generate -n AddUserProfile -d dist/db/data-source.js
# Create empty migration manually
npx typeorm migration:create -n AddIndexes
# Apply
npx typeorm migration:run -d dist/db/data-source.js
# Revert last
npx typeorm migration:revert -d dist/db/data-source.js
Query Builder
const postRepository = AppDataSource.getRepository(Post)
// Find with pagination
async function findPosts(opts: { page: number; limit: number; search?: string }) {
const { page, limit, search } = opts
const qb = postRepository.createQueryBuilder('post')
.leftJoinAndSelect('post.author', 'author')
.leftJoinAndSelect('post.tags', 'tag')
.where('post.published = :published', { published: true })
.orderBy('post.createdAt', 'DESC')
.skip((page - 1) * limit)
.take(limit)
if (search) {
qb.andWhere(
'post.title ILIKE :search OR post.content ILIKE :search',
{ search: `%${search}%` }
)
}
const [items, total] = await qb.getManyAndCount()
return { items, total, pages: Math.ceil(total / limit) }
}
Subscribers (hooks)
import { EntitySubscriberInterface, EventSubscriber, InsertEvent, UpdateEvent } from 'typeorm'
@EventSubscriber()
export class PostSubscriber implements EntitySubscriberInterface<Post> {
listenTo() { return Post }
async afterInsert(event: InsertEvent<Post>) {
await SearchIndexer.index('posts', event.entity)
}
async afterUpdate(event: UpdateEvent<Post>) {
if (event.updatedColumns.some(c => c.propertyName === 'published')) {
await NotificationService.notifyFollowers(event.entity)
}
}
}
NestJS integration
// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm'
@Module({
imports: [
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
url: config.get('DATABASE_URL'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
migrations: [__dirname + '/db/migrations/*{.ts,.js}'],
migrationsRun: true,
synchronize: false,
})
}),
TypeOrmModule.forFeature([User, Post])
]
})
export class AppModule {}
Timelines
Basic TypeORM setup with entities, migrations and repositories: 1–2 days. Integration into NestJS project with modules and tests: 2–3 days. Migrating existing project from another ORM to TypeORM: 3–5 days.







