Appearance
Database Integration
NestJS supports multiple database solutions. This chapter covers the most popular options: TypeORM, Prisma, and Mongoose for MongoDB.
┌─────────────────────────────────────────────────────────────────┐
│ DATABASE OPTIONS IN NESTJS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ TypeORM │ │ Prisma │ │ Mongoose │ │
│ │ │ │ │ │ │ │
│ │ PostgreSQL │ │ PostgreSQL │ │ MongoDB │ │
│ │ MySQL │ │ MySQL │ │ │ │
│ │ SQLite │ │ SQLite │ │ │ │
│ │ SQL Server │ │ SQL Server │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ Which to choose? │
│ • TypeORM: Most features, good for complex SQL │
│ • Prisma: Modern, type-safe, great developer experience │
│ • Mongoose: MongoDB only, schema-based │
│ │
└─────────────────────────────────────────────────────────────────┘TypeORM Integration
TypeORM is a powerful ORM that supports many databases including PostgreSQL, MySQL, SQLite, and more.
Installation
bash
# Install TypeORM and database driver
npm install @nestjs/typeorm typeorm
# For PostgreSQL
npm install pg
# For MySQL
npm install mysql2
# For SQLite
npm install better-sqlite3Configuration
typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'password',
database: 'myapp',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true, // Don't use in production!
}),
],
})
export class AppModule {}Async Configuration
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(),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_NAME'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: configService.get('NODE_ENV') !== 'production',
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}Creating Entities
typescript
// src/users/entities/user.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
BeforeInsert,
} from 'typeorm';
import { Post } from '../../posts/entities/post.entity';
import * as bcrypt from 'bcrypt';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 100 })
name: string;
@Column({ unique: true })
email: string;
@Column({ select: false }) // Don't include in queries by default
password: string;
@Column({ default: true })
isActive: boolean;
@Column({ type: 'enum', enum: ['user', 'admin'], default: 'user' })
role: string;
@OneToMany(() => Post, post => post.author)
posts: Post[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@BeforeInsert()
async hashPassword() {
if (this.password) {
this.password = await bcrypt.hash(this.password, 10);
}
}
}
// src/posts/entities/post.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
CreateDateColumn,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column('text')
content: string;
@Column({ default: false })
published: boolean;
@ManyToOne(() => User, user => user.posts)
author: User;
@Column()
authorId: number;
@CreateDateColumn()
createdAt: Date;
}Repository Pattern
typescript
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}typescript
// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
async create(createUserDto: CreateUserDto): Promise<User> {
const user = this.usersRepository.create(createUserDto);
return this.usersRepository.save(user);
}
async findAll(): Promise<User[]> {
return this.usersRepository.find({
relations: ['posts'],
});
}
async findOne(id: number): Promise<User> {
const user = await this.usersRepository.findOne({
where: { id },
relations: ['posts'],
});
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
return user;
}
async findByEmail(email: string): Promise<User | null> {
return this.usersRepository.findOne({
where: { email },
select: ['id', 'email', 'password', 'name', 'role'],
});
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
const user = await this.findOne(id);
Object.assign(user, updateUserDto);
return this.usersRepository.save(user);
}
async remove(id: number): Promise<void> {
const result = await this.usersRepository.delete(id);
if (result.affected === 0) {
throw new NotFoundException(`User #${id} not found`);
}
}
}Query Builder
typescript
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
async findWithFilters(filters: {
role?: string;
isActive?: boolean;
search?: string;
page?: number;
limit?: number;
}): Promise<{ users: User[]; total: number }> {
const { role, isActive, search, page = 1, limit = 10 } = filters;
const queryBuilder = this.usersRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'posts');
if (role) {
queryBuilder.andWhere('user.role = :role', { role });
}
if (isActive !== undefined) {
queryBuilder.andWhere('user.isActive = :isActive', { isActive });
}
if (search) {
queryBuilder.andWhere(
'(user.name ILIKE :search OR user.email ILIKE :search)',
{ search: `%${search}%` },
);
}
const [users, total] = await queryBuilder
.orderBy('user.createdAt', 'DESC')
.skip((page - 1) * limit)
.take(limit)
.getManyAndCount();
return { users, total };
}
async getUserStats(): Promise<any> {
return this.usersRepository
.createQueryBuilder('user')
.select('user.role', 'role')
.addSelect('COUNT(*)', 'count')
.groupBy('user.role')
.getRawMany();
}
}Transactions
typescript
import { DataSource } from 'typeorm';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
private dataSource: DataSource,
) {}
async createWithTransaction(createUserDto: CreateUserDto): Promise<User> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const user = queryRunner.manager.create(User, createUserDto);
await queryRunner.manager.save(user);
// Create related entities
const profile = queryRunner.manager.create(Profile, {
userId: user.id,
});
await queryRunner.manager.save(profile);
await queryRunner.commitTransaction();
return user;
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
}Prisma Integration
Prisma is a modern ORM with excellent TypeScript support and an intuitive data model.
Installation
bash
npm install prisma --save-dev
npm install @prisma/client
# Initialize Prisma
npx prisma initSchema Definition
prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
password String
role Role @default(USER)
isActive Boolean @default(true)
posts Post[]
profile Profile?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Profile {
id Int @id @default(autoincrement())
bio String?
avatar String?
user User @relation(fields: [userId], references: [id])
userId Int @unique
}
model Post {
id Int @id @default(autoincrement())
title String
content String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
}
enum Role {
USER
ADMIN
}Prisma Service
typescript
// src/prisma/prisma.service.ts
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();
}
}
// src/prisma/prisma.module.ts
import { Module, Global } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}Using Prisma in Services
typescript
// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { Prisma } from '@prisma/client';
import * as bcrypt from 'bcrypt';
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
async create(createUserDto: CreateUserDto) {
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
return this.prisma.user.create({
data: {
...createUserDto,
password: hashedPassword,
},
select: {
id: true,
email: true,
name: true,
role: true,
createdAt: true,
},
});
}
async findAll(params: {
skip?: number;
take?: number;
where?: Prisma.UserWhereInput;
orderBy?: Prisma.UserOrderByWithRelationInput;
}) {
const { skip, take, where, orderBy } = params;
return this.prisma.user.findMany({
skip,
take,
where,
orderBy,
include: {
posts: true,
profile: true,
},
});
}
async findOne(id: number) {
const user = await this.prisma.user.findUnique({
where: { id },
include: {
posts: true,
profile: true,
},
});
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
return user;
}
async update(id: number, updateUserDto: UpdateUserDto) {
try {
return await this.prisma.user.update({
where: { id },
data: updateUserDto,
});
} catch (error) {
if (error.code === 'P2025') {
throw new NotFoundException(`User #${id} not found`);
}
throw error;
}
}
async remove(id: number) {
try {
return await this.prisma.user.delete({
where: { id },
});
} catch (error) {
if (error.code === 'P2025') {
throw new NotFoundException(`User #${id} not found`);
}
throw error;
}
}
}Prisma Transactions
typescript
async createUserWithProfile(data: CreateUserWithProfileDto) {
return this.prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: {
email: data.email,
name: data.name,
password: await bcrypt.hash(data.password, 10),
},
});
const profile = await tx.profile.create({
data: {
userId: user.id,
bio: data.bio,
},
});
return { user, profile };
});
}Prisma Migrations
bash
# Create a migration
npx prisma migrate dev --name init
# Apply migrations in production
npx prisma migrate deploy
# Generate Prisma Client
npx prisma generate
# Open Prisma Studio (GUI)
npx prisma studioMongoDB with Mongoose
For MongoDB, NestJS provides integration with Mongoose ODM.
Installation
bash
npm install @nestjs/mongoose mongooseConfiguration
typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/myapp'),
],
})
export class AppModule {}
// Async configuration
@Module({
imports: [
MongooseModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
uri: configService.get('MONGODB_URI'),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}Creating Schemas
typescript
// src/users/schemas/user.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
export type UserDocument = User & Document;
@Schema({ timestamps: true })
export class User {
@Prop({ required: true })
name: string;
@Prop({ required: true, unique: true })
email: string;
@Prop({ required: true, select: false })
password: string;
@Prop({ default: true })
isActive: boolean;
@Prop({ type: String, enum: ['user', 'admin'], default: 'user' })
role: string;
@Prop({ type: [{ type: Types.ObjectId, ref: 'Post' }] })
posts: Types.ObjectId[];
}
export const UserSchema = SchemaFactory.createForClass(User);
// Add index
UserSchema.index({ email: 1 });
// Add virtual
UserSchema.virtual('fullName').get(function() {
return `${this.name}`;
});
// Pre-save hook
UserSchema.pre('save', async function(next) {
if (this.isModified('password')) {
const bcrypt = require('bcrypt');
this.password = await bcrypt.hash(this.password, 10);
}
next();
});Using Mongoose in Services
typescript
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from './schemas/user.schema';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from './schemas/user.schema';
import { CreateUserDto } from './dto/create-user.dto';
@Injectable()
export class UsersService {
constructor(
@InjectModel(User.name)
private userModel: Model<UserDocument>,
) {}
async create(createUserDto: CreateUserDto): Promise<User> {
const createdUser = new this.userModel(createUserDto);
return createdUser.save();
}
async findAll(): Promise<User[]> {
return this.userModel.find().populate('posts').exec();
}
async findOne(id: string): Promise<User> {
const user = await this.userModel.findById(id).populate('posts').exec();
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
return user;
}
async findByEmail(email: string): Promise<User | null> {
return this.userModel.findOne({ email }).select('+password').exec();
}
async update(id: string, updateUserDto: UpdateUserDto): Promise<User> {
const user = await this.userModel
.findByIdAndUpdate(id, updateUserDto, { new: true })
.exec();
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
return user;
}
async remove(id: string): Promise<void> {
const result = await this.userModel.findByIdAndDelete(id).exec();
if (!result) {
throw new NotFoundException(`User #${id} not found`);
}
}
async findWithFilters(filters: any): Promise<User[]> {
const query = this.userModel.find();
if (filters.role) {
query.where('role').equals(filters.role);
}
if (filters.isActive !== undefined) {
query.where('isActive').equals(filters.isActive);
}
if (filters.search) {
query.or([
{ name: new RegExp(filters.search, 'i') },
{ email: new RegExp(filters.search, 'i') },
]);
}
return query
.skip((filters.page - 1) * filters.limit)
.limit(filters.limit)
.sort({ createdAt: -1 })
.exec();
}
}Database Seeding
TypeORM Seeding
typescript
// src/database/seeds/user.seed.ts
import { DataSource } from 'typeorm';
import { User } from '../../users/entities/user.entity';
import * as bcrypt from 'bcrypt';
export async function seedUsers(dataSource: DataSource) {
const userRepository = dataSource.getRepository(User);
const users = [
{
name: 'Admin User',
email: 'admin@example.com',
password: await bcrypt.hash('password123', 10),
role: 'admin',
},
{
name: 'Regular User',
email: 'user@example.com',
password: await bcrypt.hash('password123', 10),
role: 'user',
},
];
for (const userData of users) {
const exists = await userRepository.findOne({
where: { email: userData.email },
});
if (!exists) {
const user = userRepository.create(userData);
await userRepository.save(user);
}
}
console.log('Users seeded successfully');
}Prisma Seeding
typescript
// prisma/seed.ts
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';
const prisma = new PrismaClient();
async function main() {
const adminPassword = await bcrypt.hash('password123', 10);
const userPassword = await bcrypt.hash('password123', 10);
const admin = await prisma.user.upsert({
where: { email: 'admin@example.com' },
update: {},
create: {
email: 'admin@example.com',
name: 'Admin User',
password: adminPassword,
role: 'ADMIN',
},
});
const user = await prisma.user.upsert({
where: { email: 'user@example.com' },
update: {},
create: {
email: 'user@example.com',
name: 'Regular User',
password: userPassword,
role: 'USER',
},
});
console.log({ admin, user });
}
main()
.catch(e => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});Add to package.json:
json
{
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}Run: npx prisma db seed
Best Practices
1. Use Repositories/Services Layer
typescript
// ✅ Good - Service handles database logic
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
findOne(id: number) {
return this.prisma.user.findUnique({ where: { id } });
}
}
// ❌ Bad - Controller directly accesses database
@Controller('users')
export class UsersController {
constructor(private prisma: PrismaService) {}
@Get(':id')
findOne(@Param('id') id: string) {
return this.prisma.user.findUnique({ where: { id: +id } });
}
}2. Handle Database Errors
typescript
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
@Injectable()
export class UsersService {
async create(data: CreateUserDto) {
try {
return await this.prisma.user.create({ data });
} catch (error) {
if (error.code === 'P2002') {
throw new ConflictException('Email already exists');
}
throw error;
}
}
}3. Use Environment Variables
typescript
// .env
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
// config
TypeOrmModule.forRoot({
url: process.env.DATABASE_URL,
// ...
})Summary
In this chapter, you learned:
- TypeORM integration with entities and repositories
- Prisma ORM setup and usage
- MongoDB with Mongoose
- Query building and transactions
- Database seeding
- Best practices for database operations
What's Next?
In the next chapter, we'll learn about Authentication and understand:
- JWT authentication
- Passport integration
- Auth guards
- Session management