Skip to content

nest_13_database - Database Integration

Your API is useless without data persistence NestJS gives you first-party modules for TypeORM , Mongoose , Prisma , and Sequelize - each integrates through the same module/provider pattern , so your DI container knows about your database connections and repositories without you manually wiring every query

what's in here

  • @nestjs/typeorm setup and entities
  • @nestjs/mongoose for MongoDB
  • @nestjs/prisma integration
  • Repository pattern and dependency injection
  • Database migrations
  • Transactions and error handling
  • Connection pooling and configuration

TypeORM with PostgreSQL

npm install @nestjs/typeorm typeorm pg

defining an entity

import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm'

@Entity('users')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Column({ unique: true })
  email: string

  @Column({ select: false })   // excluded from SELECT queries by default
  passwordHash: string

  @Column({ default: 'user' })
  role: string

  @Column({ default: true })
  isActive: boolean

  @OneToMany(() => Order, order => order.user)
  orders: Order[]

  @CreateDateColumn()
  createdAt: Date

  @UpdateDateColumn()
  updatedAt: Date
}

@Entity('users') maps the class to the users table @PrimaryGeneratedColumn('uuid') creates a UUID primary key - auto-increment integers leak your user count @Column({ select: false }) prevents passwordHash from being included in queries unless explicitly selected - if you forget this , your auth endpoint returns hashes in API responses

wiring the module

import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { User } from './entities/user.entity'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres',
        host: config.get('DB_HOST'),
        port: config.get<number>('DB_PORT'),
        username: config.get('DB_USER'),
        password: config.get('DB_PASSWORD'),
        database: config.get('DB_NAME'),
        entities: [__dirname + '/**/*.entity{.ts,.js}'],
        synchronize: config.get('NODE_ENV') !== 'production',
        // NOTE: synchronize:true in prod drops and recreates tables
        // Use migrations in production
        logging: config.get('NODE_ENV') === 'development',
        poolSize: 10,  // connection pool size
      })
    }),
    TypeOrmModule.forFeature([User])  // registers User repository
  ],
  controllers: [UsersController],
  providers: [UsersService]
})
export class UsersModule {}

forRootAsync reads database config from environment variables - never hardcode credentials synchronize: true is fine in development but will drop your production data if the schema changes Use migrations in production - they're version-controlled and reversible

repository injection

import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from './entities/user.entity'

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>
  ) {}

  async findByEmail(email: string): Promise<User | null> {
    return this.usersRepository.findOne({
      where: { email },
      relations: ['orders']  // eager-load related entities when needed
    })
  }

  async create(data: Partial<User>): Promise<User> {
    const user = this.usersRepository.create(data)
    return this.usersRepository.save(user)
  }

  async updatePassword(userId: string, newHash: string): Promise<void> {
    await this.usersRepository.update(userId, { passwordHash: newHash })
  }
}

@InjectRepository(User) gives you the TypeORM repository for the User entity Repository methods are your query interface - findOne , find , create , save , update , delete Always specify where clauses to prevent accidentally returning all rows

Mongoose (MongoDB)

npm install @nestjs/mongoose mongoose
import { Module } from '@nestjs/common'
import { MongooseModule } from '@nestjs/mongoose'
import { ConfigModule, ConfigService } from '@nestjs/config'

@Module({
  imports: [
    MongooseModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        uri: config.get('MONGODB_URI'),
        // connection pool and timeout settings
        maxPoolSize: 10,
        serverSelectionTimeoutMS: 5000,
        socketTimeoutMS: 45000,
      })
    }),
    MongooseModule.forFeature([
      { name: 'User', schema: UserSchema }
    ])
  ]
})
export class DatabaseModule {}
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
import { Document } from 'mongoose'

@Schema({ timestamps: true })
export class User extends Document {
  @Prop({ required: true, unique: true })
  email: string

  @Prop({ required: true, select: false })
  passwordHash: string

  @Prop({ default: false })
  isVerified: boolean
}

export const UserSchema = SchemaFactory.createForClass(User)

Mongoose follows the same module/provider pattern as TypeORM timestamps: true auto-adds createdAt and updatedAt fields select: false on passwordHash prevents it from being returned in queries

Prisma integration

npm install @prisma/client
npx prisma init
// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id           String   @id @default(uuid())
  email        String   @unique
  passwordHash String
  role         String   @default("user")
  isActive     Boolean  @default(true)
  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt
  orders       Order[]
}

model Order {
  id        String   @id @default(uuid())
  userId    String
  user      User     @relation(fields: [userId], references: [id])
  total     Decimal
  createdAt DateTime @default(now())
}
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect()
  }

  async onModuleDestroy() {
    await this.$disconnect()
  }
}
import { Module } from '@nestjs/common'
import { PrismaService } from './prisma.service'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'

@Module({
  controllers: [UsersController],
  providers: [PrismaService, UsersService]
})
export class UsersModule {}

Prisma gives you type-safe queries - the generated client knows your schema at TypeScript level No raw SQL strings unless you explicitly use $queryRaw TypeORM and Mongoose are more flexible ; Prisma is safer and has better DX

transactions

import { Injectable } from '@nestjs/common'
import { InjectEntityManager } from '@nestjs/typeorm'
import { EntityManager } from 'typeorm'

@Injectable()
export class OrdersService {
  constructor(
    @InjectEntityManager()
    private entityManager: EntityManager
  ) {}

  async createOrder(userId: string, items: OrderItem[]) {
    // use transaction to ensure atomicity
    await this.entityManager.transaction(async (manager) => {
      const total = items.reduce((sum, item) => sum + item.price, 0)

      // check user balance within same transaction
      const user = await manager.findOne(User, { where: { id: userId } })
      if (!user || user.balance < total) {
        throw new BadRequestException('insufficient funds')
      }

      // deduct balance and create order atomically
      await manager.update(User, userId, {
        balance: () => `balance - ${total}`  // SQL expression - race condition safe
      })

      const order = manager.create(Order, { userId, total, items })
      await manager.save(order)

      return order
    })
  }
}

Transactions ensure that all operations succeed or all fail together In money-handling code , transactions are non-negotiable - without them , a crash between checking balance and deducting creates free money The () => 'balance - ${total}' syntax uses a raw SQL expression - TypeORM generates UPDATE SET balance = balance - 100 which is atomic in the database

migrations

# TypeORM migration
npx typeorm migration:create src/migrations/CreateUsersTable
npx typeorm migration:run

# Prisma migration
npx prisma migrate dev --name add_orders_table  # development
npx prisma migrate deploy                         # production

Migration files are version-controlled SQL scripts Run them as part of deployment , not at application startup synchronize: true (TypeORM) or prisma db push should never run in production

connection pooling and security

TypeOrmModule.forRootAsync({
  useFactory: (config: ConfigService) => ({
    type: 'postgres',
    extra: {
      max: 10,                  // pool size - 10 concurrent connections
      idleTimeoutMillis: 30000,  // close idle connections after 30s
      connectionTimeoutMillis: 5000,  // fail fast if DB unreachable
    },
    ssl: config.get('NODE_ENV') === 'production'
      ? { rejectUnauthorized: true }
      : false
  })
})

Connection pooling prevents your app from exhausting database connections during traffic spikes SSL in production prevents man-in-the-middle attacks on your database traffic - yes , even if your DB is in the same VPC Connection timeouts prevent your app from hanging when the database goes down

prerequisites

nest_12_security - Security Best Practices


next -> nest_14_testing - Testing