NestJS Persistence: Mastering Database Integrations, ORMs, and Advanced Patterns
Data is the lifeblood of your application. Whether you choose SQL or NoSQL, this guide dissects the integration of major ecosystem players (TypeORM, Prisma, MikroORM, Mongoose) into NestJS. We go beyond simple connections to explore the Repository pattern, ACID transactions, and enterprise-grade read-replica configurations.
Database Integration
TypeORM Integration
TypeORM is the most mature ORM for NestJS, providing decorator-based entity definitions and seamless integration through @nestjs/typeorm. It supports Active Record and Data Mapper patterns with excellent TypeScript support.
// app.module.ts @Module({ imports: [ TypeOrmModule.forRoot({ type: 'postgres', host: 'localhost', entities: [User], synchronize: false, // NEVER true in production }), TypeOrmModule.forFeature([User]), ], }) // user.entity.ts @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column({ unique: true }) email: string; @OneToMany(() => Post, post => post.author) posts: Post[]; }
Prisma Integration
Prisma offers type-safe database access with auto-generated client, excellent migrations, and a declarative schema language—it's becoming the preferred choice for greenfield NestJS projects due to superior DX and performance.
// prisma/schema.prisma model User { id Int @id @default(autoincrement()) email String @unique posts Post[] } // prisma.service.ts @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit { async onModuleInit() { await this.$connect(); } } // user.service.ts @Injectable() export class UserService { constructor(private prisma: PrismaService) {} findAll() { return this.prisma.user.findMany({ include: { posts: true } }); } }
Mongoose/MongoDB Integration
NestJS provides first-class MongoDB support via @nestjs/mongoose, wrapping Mongoose with decorators for schema definition—ideal for document-oriented data models and rapid prototyping.
// user.schema.ts @Schema({ timestamps: true }) export class User { @Prop({ required: true, unique: true }) email: string; @Prop({ type: [{ type: Types.ObjectId, ref: 'Post' }] }) posts: Post[]; } export const UserSchema = SchemaFactory.createForClass(User); // app.module.ts @Module({ imports: [ MongooseModule.forRoot('mongodb://localhost/nest'), MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), ], })
Sequelize Integration
Sequelize is a battle-tested ORM supporting MySQL, PostgreSQL, SQLite, and MSSQL through @nestjs/sequelize, offering model-based architecture with decorators via sequelize-typescript.
// user.model.ts @Table({ tableName: 'users' }) export class User extends Model { @Column({ primaryKey: true, autoIncrement: true }) id: number; @Column({ unique: true }) email: string; @HasMany(() => Post) posts: Post[]; } // users.service.ts @Injectable() export class UsersService { constructor(@InjectModel(User) private userModel: typeof User) {} findAll(): Promise<User[]> { return this.userModel.findAll({ include: [Post] }); } }
MikroORM Integration
MikroORM is a TypeScript-first ORM with Unit of Work pattern, identity map, and excellent performance—it's gaining popularity as a modern alternative to TypeORM with better type safety.
// mikro-orm.config.ts export default defineConfig({ entities: ['./dist/**/*.entity.js'], entitiesTs: ['./src/**/*.entity.ts'], dbName: 'nestdb', type: 'postgresql', }); // user.entity.ts @Entity() export class User { @PrimaryKey() id!: number; @Property({ unique: true }) email!: string; @OneToMany(() => Post, post => post.author) posts = new Collection<Post>(this); }
Repository Pattern
The Repository pattern abstracts data access logic, providing a clean separation between business logic and database operations—NestJS ORMs typically inject repositories directly into services.
┌─────────────────────────────────────────────────────────┐
│ Controller │
└──────────────────────┬──────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────┐
│ Service │
│ (Business Logic Layer) │
└──────────────────────┬──────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────┐
│ Repository │
│ (Data Access Abstraction) │
└──────────────────────┬──────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────┐
│ Database │
└─────────────────────────────────────────────────────────┘
// users.repository.ts (Custom Repository) @Injectable() export class UsersRepository { constructor( @InjectRepository(User) private readonly repo: Repository<User>, ) {} async findByEmail(email: string): Promise<User | null> { return this.repo.findOne({ where: { email } }); } async createWithProfile(dto: CreateUserDto): Promise<User> { const user = this.repo.create(dto); return this.repo.save(user); } }
Database Transactions
Transactions ensure atomicity of multiple database operations—NestJS supports both manual transaction management and decorator-based approaches depending on your ORM choice.
// TypeORM - Manual Transaction @Injectable() export class TransferService { constructor(private dataSource: DataSource) {} async transfer(fromId: number, toId: number, amount: number) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { await queryRunner.manager.decrement(Account, { id: fromId }, 'balance', amount); await queryRunner.manager.increment(Account, { id: toId }, 'balance', amount); await queryRunner.commitTransaction(); } catch (err) { await queryRunner.rollbackTransaction(); throw err; } finally { await queryRunner.release(); } } } // Prisma - Interactive Transaction async transfer(fromId: number, toId: number, amount: number) { return this.prisma.$transaction(async (tx) => { await tx.account.update({ where: { id: fromId }, data: { balance: { decrement: amount } } }); await tx.account.update({ where: { id: toId }, data: { balance: { increment: amount } } }); }); }
Migrations
Migrations provide version control for database schemas, enabling safe and reversible schema changes across environments—essential for production deployments and team collaboration.
# TypeORM npx typeorm migration:generate -d ./data-source.ts ./migrations/AddUserTable npx typeorm migration:run -d ./data-source.ts # Prisma npx prisma migrate dev --name add_user_table npx prisma migrate deploy # Production
// TypeORM Migration Example export class AddUserTable1699999999999 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createTable(new Table({ name: 'users', columns: [ { name: 'id', type: 'int', isPrimary: true, isGenerated: true }, { name: 'email', type: 'varchar', isUnique: true }, { name: 'created_at', type: 'timestamp', default: 'now()' }, ], })); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropTable('users'); } }
Seeding
Seeding populates databases with initial or test data—while not built into NestJS, libraries like typeorm-seeding or custom scripts using Faker provide robust solutions.
// seeds/user.seed.ts import { Seeder, SeederFactoryManager } from 'typeorm-extension'; import { DataSource } from 'typeorm'; import { User } from '../entities/user.entity'; import { faker } from '@faker-js/faker'; export class UserSeeder implements Seeder { async run(dataSource: DataSource, factoryManager: SeederFactoryManager) { const repo = dataSource.getRepository(User); // Create admin user await repo.insert({ email: 'admin@example.com', role: 'admin' }); // Create fake users const users = Array.from({ length: 50 }, () => ({ email: faker.internet.email(), role: 'user', })); await repo.insert(users); } }
Multiple Database Connections
NestJS supports multiple database connections for multi-tenant architectures, read/write splitting, or polyglot persistence—each connection is named and injected explicitly.
@Module({ imports: [ TypeOrmModule.forRoot({ name: 'primary', type: 'postgres', host: 'primary-db.example.com', database: 'main', entities: [User, Order], }), TypeOrmModule.forRoot({ name: 'analytics', type: 'postgres', host: 'analytics-db.example.com', database: 'analytics', entities: [Event, Metric], }), TypeOrmModule.forFeature([User], 'primary'), TypeOrmModule.forFeature([Event], 'analytics'), ], }) export class AppModule {} // Injection constructor( @InjectRepository(User, 'primary') private userRepo: Repository<User>, @InjectRepository(Event, 'analytics') private eventRepo: Repository<Event>, ) {}
Read Replicas Configuration
Read replicas distribute read queries across multiple database instances to improve performance and availability—TypeORM supports this natively through replication configuration.
TypeOrmModule.forRoot({ type: 'postgres', replication: { master: { host: 'master.db.example.com', port: 5432, username: 'admin', password: 'secret', database: 'production', }, slaves: [ { host: 'replica-1.db.example.com', port: 5432, username: 'reader', password: 'secret', database: 'production' }, { host: 'replica-2.db.example.com', port: 5432, username: 'reader', password: 'secret', database: 'production' }, ], }, entities: [User, Order], }) // Query routing happens automatically: // - SELECT queries → random slave // - INSERT/UPDATE/DELETE → master
┌────────────────────┐
│ Application │
└─────────┬──────────┘
│
┌─────▼─────┐
│ TypeORM │
│ Driver │
└─────┬─────┘
│
┌─────┴─────────────────┐
│ │
┌───▼───┐ ┌───────▼───────┐
│Master │◄─────────►│ Replicas │
│(Write)│ Async │ (Read x N) │
└───────┘ Repl └───────────────┘