# 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. - Published: 2026-06-19 - Updated: 2026-06-19 - Author: SharpSkill - Tags: nestjs, typeorm, migrations, database, typescript, backend - Reading time: 9 min --- NestJS TypeORM remains one of the most battle-tested ORM integrations for enterprise Node.js backends. With [TypeORM reaching version 1.0](https://typeorm.io/blog/) in May 2026 after nearly a decade of development, and [NestJS 11](https://docs.nestjs.com/) 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 milestone** > > 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. ```typescript // src/config/typeorm.config.ts 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: ```typescript // src/app.module.ts 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('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`: ```json // package.json (scripts section) { "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. > **Review every generated migration** > > 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](/technologies/node-nestjs/interview-questions/database-typeorm). A `User` with multiple `Order` records demonstrates the most common pattern: ```typescript // src/entities/user.entity.ts 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; } ``` ```typescript // src/entities/order.entity.ts 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: ```typescript // src/entities/product.entity.ts 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. ## Repository Pattern and Custom Repositories The [NestJS module and DI system](/technologies/node-nestjs/interview-questions/nestjs-modules-di) integrates TypeORM repositories through `TypeOrmModule.forFeature()`. Injecting the default `Repository` covers standard CRUD operations. When queries grow complex, custom repositories encapsulate that logic. ```typescript // src/orders/orders.module.ts 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 {} ``` ```typescript // src/orders/orders.service.ts 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, ) {} async findByUser(userId: string): Promise { 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 { // 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. ```typescript // src/orders/orders.service.ts (additional method) async createOrderWithItems( userId: string, items: { productId: string; quantity: number }[], dataSource: DataSource, ): Promise { 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. > **QueryRunner vs 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`?** 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](/blog/node-nestjs/nestjs-guards-interceptors-modular-architecture). ## Conclusion - Use a **separate `DataSource` config file** for the TypeORM CLI, independent from the NestJS module configuration - **Never enable `synchronize` in production** environments; rely on generated and reviewed migrations for all schema changes - Place `@ManyToOne` on the entity that owns the foreign key, and use explicit `@JoinColumn` naming for clarity in raw queries and migration output - Choose between `find()` options API for simple queries and `QueryBuilder` for complex joins, aggregations, and conditional logic - Wrap multi-table mutations in **transactions** using `DataSource.transaction()`, and ensure every operation inside the callback uses the transactional `manager` - With TypeORM 1.0 stabilizing the API surface and NestJS 11 maturing its [module architecture](/technologies/node-nestjs), this stack remains a strong choice for enterprise backends requiring structured, type-safe database access --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/node-nestjs/nestjs-typeorm-migrations-relations-best-practices