React & Next.js Topic
NestJs Connecting to Databases
NestJS Database Integration: Prisma vs TypeORM
Overview
NestJS provides excellent database integration capabilities through modules like @nestjs/typeorm and community packages for Prisma. Both ORMs offer robust type safety and developer experience.
1. Setting Up Prisma
Installation
bash
npm install prisma @prisma/client npm install @nestjs/config # For environment variables npx prisma init
Configuration
prisma/schema.prisma
prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql" # or mysql, sqlite, etc.
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
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())
updatedAt DateTime @updatedAt
}
prisma.module.ts
typescript
import { Module, Global } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global() // Make PrismaService available globally
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
prisma.service.ts
typescript
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
constructor() {
super({
log: ['query', 'info', 'warn', 'error'],
});
}
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
2. Setting Up TypeORM
Installation
bash
npm install @nestjs/typeorm typeorm pg # PostgreSQL # or for MySQL: npm install mysql2
Configuration
app.module.ts
typescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'postgresql',
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', // Auto sync in dev only
logging: true,
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
3. Creating Entities/Models
TypeORM Entities
user.entity.ts
typescript
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
import { Post } from './post.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column({ nullable: true })
name: string;
@OneToMany(() => Post, post => post.author)
posts: Post[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
post.entity.ts
typescript
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column({ nullable: true })
content: string;
@Column({ default: false })
published: boolean;
@ManyToOne(() => User, user => user.posts)
@JoinColumn({ name: 'authorId' })
author: User;
@Column()
authorId: number;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
Prisma Models (Already in schema.prisma)
4. Repository Pattern Implementation
TypeORM with Custom Repository Pattern
base.interface.repository.ts
typescript
import { Repository, FindOneOptions, FindManyOptions } from 'typeorm';
export interface IBaseRepository<T> {
create(data: Partial<T>): Promise<T>;
findOne(id: number): Promise<T | null>;
findOneBy(where: FindOneOptions<T>): Promise<T | null>;
findAll(options?: FindManyOptions<T>): Promise<T[]>;
update(id: number, data: Partial<T>): Promise<T>;
delete(id: number): Promise<void>;
}
export abstract class BaseRepository<T> implements IBaseRepository<T> {
constructor(protected readonly repository: Repository<T>) {}
async create(data: Partial<T>): Promise<T> {
const entity = this.repository.create(data as any);
return await this.repository.save(entity);
}
async findOne(id: number): Promise<T | null> {
return await this.repository.findOne({ where: { id } } as any);
}
async findOneBy(where: FindOneOptions<T>): Promise<T | null> {
return await this.repository.findOne(where);
}
async findAll(options?: FindManyOptions<T>): Promise<T[]> {
return await this.repository.find(options);
}
async update(id: number, data: Partial<T>): Promise<T> {
await this.repository.update(id, data as any);
return await this.findOne(id);
}
async delete(id: number): Promise<void> {
await this.repository.delete(id);
}
}
user.repository.ts
typescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { BaseRepository } from './base.interface.repository';
@Injectable()
export class UserRepository extends BaseRepository<User> {
constructor(
@InjectRepository(User)
private readonly userRepo: Repository<User>,
) {
super(userRepo);
}
async findByEmail(email: string): Promise<User | null> {
return await this.userRepo.findOne({ where: { email } });
}
async findWithPosts(userId: number): Promise<User | null> {
return await this.userRepo.findOne({
where: { id: userId },
relations: ['posts'],
});
}
}
user.module.ts
typescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UserRepository } from './user.repository';
import { UserService } from './user.service';
import { UserController } from './user.controller';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UserRepository, UserService],
controllers: [UserController],
exports: [UserRepository],
})
export class UserModule {}
Prisma with Repository Pattern
prisma/user.repository.ts
typescript
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma.service';
import { User, Prisma } from '@prisma/client';
@Injectable()
export class UserRepository {
constructor(private prisma: PrismaService) {}
async create(data: Prisma.UserCreateInput): Promise<User> {
return await this.prisma.user.create({
data,
});
}
async findOne(id: number): Promise<User | null> {
return await this.prisma.user.findUnique({
where: { id },
});
}
async findOneBy(where: Prisma.UserWhereInput): Promise<User | null> {
return await this.prisma.user.findFirst({
where,
});
}
async findAll(params: {
skip?: number;
take?: number;
cursor?: Prisma.UserWhereUniqueInput;
where?: Prisma.UserWhereInput;
orderBy?: Prisma.UserOrderByWithRelationInput;
}): Promise<User[]> {
const { skip, take, cursor, where, orderBy } = params;
return await this.prisma.user.findMany({
skip,
take,
cursor,
where,
orderBy,
});
}
async update(id: number, data: Prisma.UserUpdateInput): Promise<User> {
return await this.prisma.user.update({
where: { id },
data,
});
}
async delete(id: number): Promise<User> {
return await this.prisma.user.delete({
where: { id },
});
}
async findByEmail(email: string): Promise<User | null> {
return await this.prisma.user.findUnique({
where: { email },
});
}
async findWithPosts(userId: number): Promise<User | null> {
return await this.prisma.user.findUnique({
where: { id: userId },
include: { posts: true },
});
}
}
5. Service Layer with CRUD Operations
TypeORM Service
user.service.ts
typescript
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
async create(createUserDto: CreateUserDto) {
// Check if user already exists
const existingUser = await this.userRepository.findByEmail(createUserDto.email);
if (existingUser) {
throw new ConflictException('User with this email already exists');
}
return await this.userRepository.create(createUserDto);
}
async findAll() {
return await this.userRepository.findAll();
}
async findOne(id: number) {
const user = await this.userRepository.findOne(id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async update(id: number, updateUserDto: UpdateUserDto) {
await this.findOne(id); // Verify user exists
return await this.userRepository.update(id, updateUserDto);
}
async remove(id: number) {
await this.findOne(id); // Verify user exists
await this.userRepository.delete(id);
}
async findUserWithPosts(id: number) {
const user = await this.userRepository.findWithPosts(id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
}
Prisma Service
prisma/user.service.ts
typescript
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
async create(createUserDto: CreateUserDto) {
const existingUser = await this.userRepository.findByEmail(createUserDto.email);
if (existingUser) {
throw new ConflictException('User with this email already exists');
}
return await this.userRepository.create({
email: createUserDto.email,
name: createUserDto.name,
});
}
async findAll(params: { page?: number; limit?: number }) {
const { page = 1, limit = 10 } = params;
const skip = (page - 1) * limit;
return await this.userRepository.findAll({
skip,
take: limit,
orderBy: { createdAt: 'desc' },
});
}
async findOne(id: number) {
const user = await this.userRepository.findOne(id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async update(id: number, updateUserDto: UpdateUserDto) {
await this.findOne(id); // Verify user exists
return await this.userRepository.update(id, updateUserDto);
}
async remove(id: number) {
await this.findOne(id); // Verify user exists
await this.userRepository.delete(id);
}
async findUserWithPosts(id: number) {
const user = await this.userRepository.findWithPosts(id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
}
6. Controller Layer
user.controller.ts
typescript
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, ParseIntPipe } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
@Get()
findAll(
@Query('page', new ParseIntPipe({ optional: true })) page?: number,
@Query('limit', new ParseIntPipe({ optional: true })) limit?: number,
) {
return this.userService.findAll({ page, limit });
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.userService.findOne(id);
}
@Get(':id/posts')
findUserWithPosts(@Param('id', ParseIntPipe) id: number) {
return this.userService.findUserWithPosts(id);
}
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
) {
return this.userService.update(id, updateUserDto);
}
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.userService.remove(id);
}
}
7. DTOs and Validation
user.dto.ts
typescript
import { IsEmail, IsString, IsOptional, MinLength, MaxLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty()
@IsEmail()
email: string;
@ApiProperty({ required: false })
@IsString()
@IsOptional()
@MinLength(2)
@MaxLength(50)
name?: string;
}
export class UpdateUserDto {
@ApiProperty({ required: false })
@IsString()
@IsOptional()
@MinLength(2)
@MaxLength(50)
name?: string;
}
8. Migration Commands
Prisma Migrations
bash
# Generate migration npx prisma migrate dev --name init # Apply migrations npx prisma migrate deploy # Generate Prisma Client npx prisma generate # Studio GUI npx prisma studio
TypeORM Migrations
bash
# Generate migration npm run typeorm migration:generate -- src/migrations/Init # Run migrations npm run typeorm migration:run # Revert migration npm run typeorm migration:revert # Create migration manually npm run typeorm migration:create -- src/migrations/AddNewColumn
Comparison Table
| Feature | TypeORM | Prisma |
|---|---|---|
| Type Safety | Good | Excellent (auto-generated types) |
| Migrations | Built-in | Built-in (declarative) |
| Relations | Decorator-based | Schema-based |
| Performance | Good | Excellent (optimized queries) |
| Learning Curve | Moderate | Gentle |
| Community | Large | Growing rapidly |
| NestJS Integration | Official package | Community packages |
| Database Support | Many databases | 5+ databases |
| Raw Queries | Full SQL access | Limited raw query support |
Comments (0)
No comments yet. Be the first to comment!