NestJS and TypeORM in 2026: Migrations, Relations and Interview Questions
Master NestJS TypeORM integration with TypeORM 1.0 migrations, entity relations, repository patterns and common interview questions for backend developers.

NestJS TypeORM remains one of the most battle-tested ORM integrations for enterprise Node.js backends. With TypeORM reaching version 1.0 in May 2026 after nearly a decade of development, and NestJS 11 shipping Express v5 by default, the stack has matured into a stable foundation for production applications. This guide covers the essential patterns every backend developer should master: migrations, entity relations, the repository pattern, and the interview questions that come with them.
TypeORM 1.0.0, released on May 19, 2026, modernizes platform requirements to ECMAScript 2023, removes deprecated APIs, and fixes migration ordering: pending migrations now run before schema synchronization when both are enabled.
Setting Up TypeORM Migrations in NestJS 11
The first rule of production databases: never use synchronize: true. Auto-sync compares entity metadata against the live schema and applies changes directly, which risks data loss on column drops, type changes, or renamed fields. Migrations provide a versioned, reviewable, and reversible alternative.
TypeORM 1.0 requires a dedicated DataSource configuration file for the CLI, separate from the NestJS module config. The CLI cannot resolve NestJS dependency injection, so it needs a standalone entry point.
import { DataSource } from 'typeorm';
import { config } from 'dotenv';
config(); // Load .env variables
export default new DataSource({
type: 'postgres',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: ['src/**/*.entity.ts'],
migrations: ['src/migrations/*.ts'],
synchronize: false,
});The NestJS module configuration mirrors these settings but uses the TypeOrmModule.forRootAsync pattern with dependency injection:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
host: config.get('DB_HOST'),
port: config.getOrThrow<number>('DB_PORT'),
username: config.get('DB_USER'),
password: config.get('DB_PASSWORD'),
database: config.get('DB_NAME'),
autoLoadEntities: true, // Auto-register entities from feature modules
synchronize: false, // Always false in production
}),
}),
],
})
export class AppModule {}autoLoadEntities: true replaces the manual entities array by automatically registering every entity imported via TypeOrmModule.forFeature() in feature modules.
Migration Workflow: Generate, Review, Run
TypeORM 1.0 ships typeorm-ts-node-commonjs as the recommended CLI runner, replacing the older ts-node ./node_modules/typeorm/cli approach. Add these scripts to package.json:
{
"migration:generate": "typeorm-ts-node-commonjs migration:generate src/migrations/$npm_config_name -d src/config/typeorm.config.ts",
"migration:run": "typeorm-ts-node-commonjs migration:run -d src/config/typeorm.config.ts",
"migration:revert": "typeorm-ts-node-commonjs migration:revert -d src/config/typeorm.config.ts"
}The generate command diffs entity metadata against the live database and produces SQL statements. Always review the output before committing. TypeORM detects column additions, removals, type changes, index modifications, and foreign key changes, but it misses data backfills, table renames (which appear as a drop followed by a create), and anything involving database extensions or triggers.
A renamed column generates a DROP + ADD instead of an ALTER RENAME. This destroys existing data. Always inspect the SQL in the up() method and adjust manually when needed.
Entity Relations: OneToMany, ManyToOne, ManyToMany
TypeORM entity relations map directly to SQL foreign keys using decorators. Understanding the three core relation types and their options is fundamental for any NestJS database module.
A User with multiple Order records demonstrates the most common pattern:
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn } from 'typeorm';
import { Order } from './order.entity';
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column()
name: string;
@OneToMany(() => Order, (order) => order.user)
orders: Order[]; // No DB column created here; relation lives on the Order side
@CreateDateColumn()
createdAt: Date;
}import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';
@Entity('orders')
export class Order {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column('decimal', { precision: 10, scale: 2 })
total: number;
@Column({ default: 'pending' })
status: string;
@ManyToOne(() => User, (user) => user.orders, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' }) // Explicit FK column name
user: User;
}The @ManyToOne side owns the foreign key. @JoinColumn is optional on @ManyToOne (TypeORM infers it), but explicit naming avoids surprises when the column name matters for raw queries or migration diffs.
For many-to-many relations, TypeORM creates a junction table automatically:
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Category } from './category.entity';
@Entity('products')
export class Product {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column('decimal', { precision: 10, scale: 2 })
price: number;
@ManyToMany(() => Category, (category) => category.products)
@JoinTable({ name: 'product_categories' }) // Owns the junction table
categories: Category[];
}@JoinTable must appear on exactly one side of the relation. It defines which entity owns the junction table and controls its name.
Ready to ace your Node.js / NestJS interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Repository Pattern and Custom Repositories
The NestJS module and DI system integrates TypeORM repositories through TypeOrmModule.forFeature(). Injecting the default Repository<T> covers standard CRUD operations. When queries grow complex, custom repositories encapsulate that logic.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Order } from '../entities/order.entity';
import { OrdersService } from './orders.service';
import { OrdersController } from './orders.controller';
@Module({
imports: [TypeOrmModule.forFeature([Order])],
providers: [OrdersService],
controllers: [OrdersController],
})
export class OrdersModule {}import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Order } from '../entities/order.entity';
@Injectable()
export class OrdersService {
constructor(
@InjectRepository(Order)
private readonly orderRepo: Repository<Order>,
) {}
async findByUser(userId: string): Promise<Order[]> {
return this.orderRepo.find({
where: { user: { id: userId } },
relations: ['user'], // Eager-load the user relation
order: { createdAt: 'DESC' }, // Most recent first
});
}
async findWithFilters(status: string, minTotal: number): Promise<Order[]> {
// QueryBuilder for complex queries
return this.orderRepo
.createQueryBuilder('order')
.leftJoinAndSelect('order.user', 'user')
.where('order.status = :status', { status })
.andWhere('order.total >= :minTotal', { minTotal })
.orderBy('order.total', 'DESC')
.getMany();
}
}The find() API handles straightforward queries. The QueryBuilder handles joins, subqueries, aggregations, and anything that requires fine-grained SQL control. Mixing both in the same service is perfectly valid: use find() for simple lookups and QueryBuilder when the query cannot be expressed through the options API.
Transaction Management for Data Integrity
Operations that modify multiple tables need transactions. TypeORM offers two approaches: the DataSource.transaction() method and the QueryRunner API.
async createOrderWithItems(
userId: string,
items: { productId: string; quantity: number }[],
dataSource: DataSource,
): Promise<Order> {
return dataSource.transaction(async (manager) => {
// All operations use the transactional manager
const order = manager.create(Order, {
user: { id: userId },
total: 0,
status: 'pending',
});
const savedOrder = await manager.save(order);
let total = 0;
for (const item of items) {
const product = await manager.findOneByOrFail(Product, { id: item.productId });
total += product.price * item.quantity;
await manager.save(OrderItem, {
order: savedOrder,
product,
quantity: item.quantity,
unitPrice: product.price,
});
}
savedOrder.total = total;
return manager.save(savedOrder);
});
}The callback-based transaction() method automatically commits on success and rolls back on any thrown error. Every database operation inside the callback must use the provided manager instead of the injected repository, otherwise those operations run outside the transaction.
Use DataSource.transaction() for straightforward multi-step operations. Switch to QueryRunner when manual control over commit/rollback points or savepoints is required, such as partial rollbacks in batch processing.
Common NestJS TypeORM Interview Questions
These questions appear frequently in backend developer interviews. Each answer targets the depth expected in a senior-level technical discussion.
Why should synchronize: true never be used in production?
Auto-sync applies destructive schema changes without confirmation. Dropping a column, changing a type, or removing an index happens immediately against live data. Migrations provide version control, code review, and rollback capability. The only safe use of synchronize is local development with disposable data.
What is the difference between eager and lazy relations?
Eager relations load automatically with every query on the parent entity. Lazy relations return a Promise and execute a separate query only when accessed. Eager loading causes N+1-style overhead when the related data is not needed. Lazy loading defers the cost but can trigger unexpected queries deep in the call stack. The explicit relations option in find() or leftJoinAndSelect in QueryBuilder gives full control over what gets loaded per query.
How does cascade work in TypeORM relations?
Setting cascade: true on a relation means that saving the parent entity also persists unsaved child entities. cascade: ['insert', 'update'] limits the behavior to specific operations. Cascades operate at the ORM level, not the database level. For database-level cascading deletes, use onDelete: 'CASCADE' on the @ManyToOne decorator, which generates an ON DELETE CASCADE constraint in the migration.
When should a custom repository be used instead of the default Repository<T>?
Custom repositories encapsulate complex query logic that does not belong in services. Signs to extract a custom repository: the same QueryBuilder chain appears in multiple services, the query involves multiple joins or subqueries, or the query requires raw SQL. Simple find() calls do not justify a custom repository.
For more NestJS architecture questions, see the complete NestJS guards and interceptors guide.
Ready to ace your Node.js / NestJS interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Conclusion
- Use a separate
DataSourceconfig file for the TypeORM CLI, independent from the NestJS module configuration - Never enable
synchronizein production environments; rely on generated and reviewed migrations for all schema changes - Place
@ManyToOneon the entity that owns the foreign key, and use explicit@JoinColumnnaming for clarity in raw queries and migration output - Choose between
find()options API for simple queries andQueryBuilderfor complex joins, aggregations, and conditional logic - Wrap multi-table mutations in transactions using
DataSource.transaction(), and ensure every operation inside the callback uses the transactionalmanager - With TypeORM 1.0 stabilizing the API surface and NestJS 11 maturing its module architecture, this stack remains a strong choice for enterprise backends requiring structured, type-safe database access
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Tags
Share
Related articles

NestJS + Prisma: The Modern Backend Stack for Node.js
Complete guide to building a modern backend API with NestJS and Prisma. Setup, models, services, transactions and best practices explained.

NestJS: Building a Complete REST API
Complete guide to building a professional REST API with NestJS. Controllers, Services, Modules, validation with class-validator and error handling explained.

Microservices with NestJS in 2026: Architecture, gRPC and Interview Questions
A practical guide to NestJS microservices architecture with gRPC, covering service boundaries, transport layers, streaming patterns, and common interview questions for 2026.