Hands-on Guide to Data Modeling with Prisma ORM in NestJS
Design Scalable Database Structures, Optimize Query Performance, and Drive Application Efficiency with Prisma ORM
Introduction
In the world of backend engineering, effective data modeling is a crucial aspect of building robust and scalable applications. Properly structuring and organizing your data sets the foundation for efficient data management and enables seamless interactions with your application's database. In this article, we will explore the realm of data modeling in the context of NestJS, and delve into the power of Prisma ORM.
Understanding Data Modeling
Data modeling is the process of designing the structure and organization of data within a database system. It involves defining the entities, attributes, relationships, and constraints that determine how data is stored, retrieved, and manipulated. A well-designed data model ensures data integrity, optimizes query performance, and provides a foundation for building complex applications.
Overview of Prisma ORM
Prisma ORM is a powerful tool that simplifies database operations and enhances data modeling capabilities in NestJS applications. It acts as a bridge between your application and the database, providing an intuitive and type-safe API for working with data. With Prisma ORM, you can seamlessly define, query, and manipulate your data using a declarative syntax, while maintaining full control over your database schema.
What this article offers you
By the end of this article, you will know to leverage Prisma ORM to its fullest potential, enabling you to design robust data models that drive the functionality and performance of your NestJS applications.
Join me as we unlock the world of data modeling with Prisma ORM in NestJS, and embark on a path toward efficient and scalable backend development.
The Project: TodoVista
In this tutorial guide, we are going to design the database for a task management app called TodoVista. This app allows users to create Todos, assign relevant tags to them, and even add comments for further collaboration. By diving into the development of TodoVista, we will showcase the practical implementation of data modeling techniques, such as defining entities, establishing relationships, and managing data associations.
Identify the Entities
From the project description, four distinct entities can be identified, which are:
User
Todo
Comment
Tag
Establish the Model Relationships
Based on the project description, the identified relationships among the entities are as follows:
User to Todo: One-to-Many relationship i.e. each user can create multiple todos.
User to Comment: One-to-Many relationship i.e. each user can make multiple comments on a Todo.
Todo to Comment: One-to-Many relationship i.e. each todo can have multiple comments.
User to Tag: One-to-Many relationship i.e. each user can create multiple tags.
Todo to Tag: Many-to-Many relationship i.e. each todo can have multiple tags and each tag can have multiple todos.
The identified relationships provide an understanding of how the models are related and how they can be queried to retrieve data associated with the relationships. Below is an entity-relationship diagram to enhance our understanding of the database design.
Setting Up Prisma ORM In NestJS
To set up Prisma ORM, we need to first install the Prisma CLI. Navigate to the root directory of your NestJS project and run the following command:
npm install prisma --save-dev
Next up is to create a Prisma project with the following command:
npx prisma init
The command does two things:
creates a new directory called
prisma
that contains a file calledschema.prisma
creates a
.env
file, by default consists of a DATABASE_URL variable, and can be used for defining other environment variables.
Now we need to connect Prisma to our database. And to do that, we will:
update the DATABASE_URL variable in the
.env
file to point to our database.update the provider in
schema.prisma
file to the database you are using.
Prisma supports all major relational databases such as PostgreSQL, MySQL, SQLite etc. And so the format of the connection URL depends on the database you are using, but the methodology remains the same. In this tutorial, we are going to be using the PostgreSQL database.
Below is the format of Postgres database connection string:
DATABASE_URL="postgresql://DB_USER_NAME:DB_USER_PASSWORD@HOST:PORT/DB_NAME?schema=public"
Now we update the DATABASE_URL
variable to connect to the localhost Postgres server:
DATABASE_URL="postgresql://timothy:password123@localhost:5432/todovista?schema=public"
Lastly, ensure that the provider
in schema.prisma
file is set to "postgres"
, which is most likely there by default.
Our schema.prisma
file should now look like this:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
Caveat: ConfigModule
For the DATABASE_URL
variable to get picked up from the .env
file, we need to ensure that ConfigModule
is configured for our NestJS project. And to do that, we first install the package from npm:
npm i --save @nestjs/config
After that, we will import the ConfigModule
into the AppModule
in app.module.ts
file and declare it as a global module for it to be available in all modules of our NestJS project.
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
... // other imports
@Module({
imports: [
... // other imports
ConfigModule.forRoot({isGlobal: true})
],
providers: []
})
export class AppModule {}
Defining Models with Prisma
Now from our identified entities and relationships, we will be defining the data models and relationships using Prisma schema language(PSL). To do that, we will add the following Prisma data model to our Prisma Schema in schema.prisma
file.
Data model for User entity:
// Data model for the User entity
model User {
id Int @id @default(autoincrement()) // Auto-incrementing unique identifier for users
email String @unique // Unique email for each user
username String // User's username
password String // User's password
createdAt DateTime @default(now()) // Creation timestamp for user
updatedAt DateTime @updatedAt // Update timestamp for user
// ******Relationships*************
todos Todo[] // One-to-many relationship with Todo entity (user can have multiple todos)
tags Tag[] // One-to-many relationship with Tag entity (user can have multiple tags)
}
Data model for Todo entity:
// Data model for the Todo entity
model Todo {
id Int @id @default(autoincrement()) // Auto-incrementing unique identifier for todos
title String // Title of the todo
description String? // Optional description for the todo
status Status @default(TODO) // Status of the todo (enum: TODO, DOING, AWAITING, DONE, DISCONTINUED)
createdAt DateTime @default(now()) // Creation timestamp for todo
updatedAt DateTime @updatedAt // Update timestamp for todo
// ******Relationships*************
userId Int // Foreign key referencing the User entity (user who created the todo)
user User @relation(fields: [userId], references: [id], onDelete: Cascade) // Relation to the User entity
comments Comment[] // One-to-many relationship with Comment entity (todo can have multiple comments)
tags Tag[] // Many-to-many relationship with Tag entity (todo can have multiple tags)
}
Data model for Comment entity:
// Data model for the Comment entity
model Comment {
id Int @id @default(autoincrement()) // Auto-incrementing unique identifier for comments
text String @db.VarChar(120) // Text of the comment (limited to 120 characters in the database)
userEmail String // User who made the comment
createdAt DateTime @default(now()) // Creation timestamp for comment
updatedAt DateTime @updatedAt // Update timestamp for comment
// ******Relationships*************
todoId Int // Foreign key referencing the Todo entity (todo to which the comment belongs)
todo Todo @relation(fields: [todoId], references: [id], onDelete: Cascade) // Relation to the Todo entity
}
Data model for Tag entity:
// Data model for the Tag entity
model Tag {
id Int @id @default(autoincrement()) // Auto-incrementing unique identifier for tags
title String @db.VarChar(20) // Title of the tag (limited to 20 characters in the database)
// ******Relationships*************
todos Todo[] // Many-to-many relationship with Todo entity (tag can be associated with multiple todos)
userId Int // Foreign key referencing the User entity (user who created the tag)
User User @relation(fields: [userId], references: [id], onDelete: Cascade) // Relation to the User entity
// ******Constraint*************
@@unique([userId, title]) // Ensuring that the combination of userId and title is unique for each tag
}
Putting it all together, our schema.prisma
file should now look like this:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
username String
password String
todos Todo[]
tags Tag[]
refresh_token String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Todo {
id Int @id @default(autoincrement())
title String
description String?
status Status @default(TODO)
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
comments Comment[]
tags Tag[]
@@unique([id, userId])
}
model Comment {
id Int @id @default(autoincrement())
text String @db.VarChar(120)
todoId Int
todo Todo @relation(fields: [todoId], references: [id], onDelete: Cascade)
userEmail String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Tag {
id Int @id @default(autoincrement())
title String @db.VarChar(20)
todos Todo[]
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
@@unique([userId, title])
}
enum Status {
TODO
DOING
AWAITING
DONE
DISCONTINUED
}
To facilitate comprehension, below is a visual representation of the schema.prisma
file generated from the Prismaliser app
Creating the Database Tables
What we need to do now is to create the actual tables in our database. And to do that, we will use Prisma Migrate. Run the following command in the root of your NestJS project directory:
npx prisma migrate dev --name first_migration
This command does two things:
Firstly, it creates a new directory named
migration
in ourprisma
directory. Which also contains a directory named after the name given to the migration, in this casefirst_migration
Secondly, it runs the SQL migration file generated in the first step against the database to create our tables.
Tip: Prisma Studio
One of the awesome features of Prisma ORM is the visual editor provided to visualize the data in our database. To access this, run this command at the root of your project:
npx prisma studio
Copy the URL output from the command and paste it into your browser. Voila! You would see all our database tables.
Application Code to Prisma
All we've done before now is connect Prisma to our database. Now we are going to connect our application code to Prisma and expose Prisma to all modules in our NestJS project. To do this, we will use the Prisma client, which exposes CRUD operations to our defined models. Install the Prisma client by running the following commands:
npm install @prisma/client
After that is installed, we will create a NestJS module called prisma, and we will also create a service file in it. Run the following command:
nest g module prisma
nest g service prisma --no-spec
You can give this module any name you like, but I decided to name it prisma
to identify with the concern with which it is created.
Next up is to go into our prisma.service.ts
file and fill in the following code
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
// Lifecycle hook: called once the module is initialized
async onModuleInit() {
// Establish a connection to the database
await this.$connect();
}
// Method to enable shutdown hooks for graceful application shutdown
async enableShutdownHooks(app: INestApplication) {
// Listen for the 'beforeExit' event of PrismaClient
this.$on('beforeExit', async () => {
// Gracefully close the Nest.js application
await app.close();
});
}
}
Now that prisma client is connected to the database, we need to expose this service to all the modules in our NestJS project.
- Firstly, export the
PrismaService
inprisma.module.ts
file and the Global decorator
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global() // add this
@Module({
providers: [PrismaService],
exports: [PrismaService] // add this
})
export class PrismaModule {}
- Secondly, import the
PrismaModule
inapp.module.ts
file
import { Module } from '@nestjs/common';
import {ConfigModule} from '@nestjs/config'
import { PrismaModule } from './prisma/prisma.module';
@Module({
imports: [
... // other imports
PrismaModule,
ConfigModule.forRoot({isGlobal: true}),
],
providers: []
})
export class AppModule {}
CRUD Operations with Prisma
Now that we have everything connected, we can now begin to write services that include queries to read and write data in our database. Below is a simple Tag service that includes API for creating and deleting a Tag.
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { CreateTagDto } from './dto';
@Injectable()
export class TagService {
constructor(private readonly prisma: PrismaService){}
// ---------CREATE A TAG---------------
async createTag(userId:number, dto:CreateTagDto){
try {
const tag = await this.prisma.tag.create({
data: {
title: dto.title,
userId: userId
}
})
return tag
} catch (error) {
if(error.code === 'P2002') throw new HttpException('Tag already exist', HttpStatus.CONFLICT)
throw new HttpException('An error occured', HttpStatus.INTERNAL_SERVER_ERROR)
}
}
// ---------------DELETE A TAG--------------
async deleteTag(userId:number, id:number){
try {
await this.prisma.tag.deleteMany({
where:{
id: id,
userId: userId
}
})
return;
} catch (error) {
throw new HttpException('An error occured', HttpStatus.INTERNAL_SERVER_ERROR)
}
}
}
Conclusion
In conclusion, we have explored the power of data modeling with Prisma ORM in NestJS, witnessing how it enhances the structure and behavior of backend applications. The implementation of TodoVista app exemplified essential concepts such as entity-relationship modeling and data associations.
The full TodoVista app is available on my GitHub repository at github.com/Timothy-py/TodoVista. Armed with this knowledge, you can now build scalable and intuitive backend applications with efficient data management. Thank you for joining me on this enlightening journey, and happy coding!