On this page

TypeORM integration and entity definitions

15 min read TextCh. 3 — Data and Persistence

TypeORM in NestJS

TypeORM is the most popular ORM for TypeScript and Node.js applications. NestJS provides first-class integration through the @nestjs/typeorm package, which wires TypeORM's connection management into the module system and exposes repositories through dependency injection.

Install the required packages:

npm install @nestjs/typeorm typeorm pg

For SQLite (development/testing) or MySQL, replace pg with better-sqlite3 or mysql2.

Configuring the database connection

Register TypeOrmModule.forRoot() in AppModule once. This creates the database connection and makes it available across the application:

TypeOrmModule.forRoot({
  type: 'postgres',
  host: 'localhost',
  port: 5432,
  username: 'postgres',
  password: 'postgres',
  database: 'bookstore',
  entities: [Book, Author, Category],
  synchronize: true, // development only!
})

The entities array lists all entity classes that TypeORM should manage. Alternatively, use autoLoadEntities: true combined with TypeOrmModule.forFeature([Book]) in each feature module — TypeORM then collects entities automatically.

Defining entities

An entity is a TypeScript class decorated with @Entity() that maps to a database table. Each property decorated with @Column() corresponds to a table column.

Primary keys

// Auto-incrementing integer (most common)
@PrimaryGeneratedColumn()
id: number;

// UUID primary key
@PrimaryGeneratedColumn('uuid')
id: string;

// Composite primary key
@PrimaryColumn()
userId: number;

@PrimaryColumn()
bookId: number;

Column types

@Column({ length: 200 })
title: string; // VARCHAR(200)

@Column('text')
description: string; // TEXT

@Column('decimal', { precision: 10, scale: 2 })
price: number; // DECIMAL(10, 2)

@Column('int')
stock: number; // INTEGER

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

@Column('jsonb', { nullable: true })
metadata: Record<string, unknown> | null; // JSONB (PostgreSQL)

@Column('simple-array')
tags: string[]; // Stored as comma-separated string

Timestamps

@CreateDateColumn()
createdAt: Date; // Auto-set on INSERT

@UpdateDateColumn()
updatedAt: Date; // Auto-updated on UPDATE

@DeleteDateColumn()
deletedAt: Date | null; // For soft deletes

Column options

Common column options:

@Column({
  name: 'cover_url',    // Custom DB column name
  nullable: true,        // Allow NULL
  unique: true,          // UNIQUE constraint
  default: 'default.jpg',
  select: false,         // Exclude from default SELECT
  insert: false,         // Cannot be set on insert
  update: false,         // Cannot be changed after insert
})
coverUrl: string | null;

Indexes

Define database indexes on entities for query performance:

import { Entity, Index } from 'typeorm';

@Entity('books')
@Index(['title', 'authorId'])          // Composite index
@Index(['isbn'], { unique: true })     // Unique index
export class Book {
  @Column()
  @Index()                              // Single column index
  title: string;
}

Embedding entities

For reusable groups of columns, use embedded entities:

export class Address {
  @Column()
  street: string;

  @Column()
  city: string;

  @Column()
  country: string;
}

@Entity()
export class Author {
  @Column(() => Address)
  address: Address;
}

TypeORM flattens the embedded columns into the parent table as addressStreet, addressCity, addressCountry.

Registering entities per module

Each feature module registers the entities it needs using TypeOrmModule.forFeature():

// books.module.ts
@Module({
  imports: [TypeOrmModule.forFeature([Book])],
  controllers: [BooksController],
  providers: [BooksService],
  exports: [TypeOrmModule], // Export so other modules can inject BookRepository
})
export class BooksModule {}

This makes the Repository<Book> injectable in BooksService:

import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

@Injectable()
export class BooksService {
  constructor(
    @InjectRepository(Book)
    private readonly booksRepository: Repository<Book>,
  ) {}
}

The DataSource

For advanced queries, transactions, and migrations, inject the DataSource directly:

import { DataSource } from 'typeorm';

@Injectable()
export class BooksService {
  constructor(
    @InjectRepository(Book)
    private readonly booksRepository: Repository<Book>,
    private readonly dataSource: DataSource,
  ) {}

  async transferBooks(fromId: number, toId: number): Promise<void> {
    await this.dataSource.transaction(async (manager) => {
      await manager.update(Book, { authorId: fromId }, { authorId: toId });
    });
  }
}

Entity listeners

TypeORM entities can define lifecycle hooks:

import { BeforeInsert, BeforeUpdate, AfterLoad } from 'typeorm';
import * as bcrypt from 'bcrypt';

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

  @BeforeInsert()
  @BeforeUpdate()
  async hashPassword(): Promise<void> {
    if (this.password) {
      this.password = await bcrypt.hash(this.password, 10);
    }
  }

  @AfterLoad()
  setFullName(): void {
    this.fullName = `${this.firstName} ${this.lastName}`;
  }
}

Available hooks: @AfterLoad, @BeforeInsert, @AfterInsert, @BeforeUpdate, @AfterUpdate, @BeforeRemove, @AfterRemove, @BeforeSoftRemove, @AfterSoftRemove, @BeforeRecover, @AfterRecover.

Soft deletes

Instead of deleting rows permanently, mark them as deleted with a timestamp:

@Entity()
export class Book {
  @DeleteDateColumn()
  deletedAt: Date | null;
}

Then use softDelete() and restore():

// Soft delete
await this.booksRepository.softDelete(id);

// Restore
await this.booksRepository.restore(id);

// Find including soft-deleted
await this.booksRepository.find({ withDeleted: true });

This is a common requirement for audit trails and compliance where data cannot be permanently deleted.

Never use synchronize in production
The synchronize: true option automatically modifies the database schema on startup. This is convenient in development but extremely dangerous in production — a single entity change could drop a column and destroy data. Always use migrations in production environments.
Column naming conventions
By default TypeORM uses camelCase for column names (createdAt becomes createdAt in the DB). Use the namingStrategy option with a SnakeCaseNamingStrategy to automatically convert all column names to snake_case (created_at), which is the PostgreSQL convention.
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  ManyToOne,
  JoinColumn,
} from 'typeorm';
import { Author } from '../authors/author.entity';

@Entity('books')
export class Book {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 200 })
  title: string;

  @Column({ unique: true, length: 13 })
  isbn: string;

  @Column('decimal', { precision: 8, scale: 2 })
  price: number;

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

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

  @ManyToOne(() => Author, (author) => author.books, {
    nullable: false,
    onDelete: 'RESTRICT',
  })
  @JoinColumn({ name: 'author_id' })
  author: Author;

  @Column({ name: 'author_id' })
  authorId: number;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}