Integrating Prisma with Express in a Clean and Scalable Manner

Camilo Salazar
6 min readDec 24, 2023

--

Learn how to seamlessly integrate Prisma with Express for a clean and scalable Node.js backend. Explore efficient data modeling, type-safe operations, and robust database interactions.

Photo by benjamin lehman on Unsplash

Welcome to a tutorial where we’ll master the art of crafting an elegant and efficient Express application seamlessly integrated with Prisma your sturdy, type-safe database toolkit! Let’s embark on a journey to not only ensure smooth communication between our Express application and the database but also to build a robust, structured, and scalable solution using the principles of Clean Architecture.

The complete code for this Node.js Express backend can be found in the following GitHub repository link, where you can explore all the files and code used to build it.

Setting up our project

Install Node.js and npm

If you haven’t already, follow the steps outlined in my previous tutorial, Building a Flexible and Scalable Node.js Backend with Express: A Step-by-Step Tutorial.

Before we dive into the integration, make sure you have Prisma installed on your project.

npm install prisma - save-dev

Initialize Prisma

Run the following command to initialize Prisma in your project:

npx prisma init

This command will create a prisma directory with a schema.prisma file.

Configure the Database

Let’s proceed with configuring the database using SQLite and setting up the .env file with the DATABASE_URL.

Open the prisma/schema.prisma file and configure the SQLite datasource:

datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}

This configuration specifies SQLite as the provider and uses the DATABASE_URL from the environment variables.

Open the .env file and configure the SQLite datasource:

DATABASE_URL="file:./dev.db"

Define Your Data Model

Let’s define the data model for the User entity with id, email, password, and age fields in the schema.prisma file.

model User {
id Int @id @default(autoincrement())
email String @unique
password String
age Int?
}

Explanation of the model:

  • User: This is the name of the entity/model.
  • id: An integer field that serves as the primary key and automatically increments.
  • email: A unique string field for the user's email address.
  • password: A string field to store the user's password.
  • age: An optional integer field representing the user's age.

The @id and @default(autoincrement()) annotations on the id field indicate that it is the primary key and will auto-increment with each new record.

The @unique annotation on the email field ensures that each email address in the database is unique.

Run Prisma Migrate

To create a migration and execute it using Prisma, follow these steps:

1.- Create a Migration: Run the following command to create a new migration. Prisma will analyze the changes in your schema (schema.prisma) and generate the necessary SQL migration files.

npx prisma migrate dev --name init

After running this command, Prisma will create a migration file named “init” if changes to the schema are detected and automatically apply those changes to the database. Additionally, the Prisma Client will be regenerated to reflect any modifications to the schema.

-- CreateTable
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"age" INTEGER
);

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

Using migrate dev is a practical choice during development, but in production environments, you might prefer a more controlled approach with deploy command.

Prisma Implementation as a Service

In this section, we’ll seamlessly integrate Prisma into the existing structure of our Node.js backend, as outlined in the blog post Building a Flexible and Scalable Node.js Backend with Express: A Step-by-Step Tutorial. This integration will empower our application with a robust and type-safe database layer.

Updating the Constructor

To begin, let’s update our constructor by adding the Prisma Client:

import { PrismaClient } from '@prisma/client';

class UserService {
constructor() {
this.prisma = new PrismaClient();
}

// ... Existing constructor code ...
}

This addition ensures that our UserService is now equipped with the Prisma Client, granting seamless access to interact with the database. It also facilitates ease of mocking our Prisma client for testing purposes—refer to my tutorial: From Unit to End-to-End Testing with Jest and Supertest.

Updating the getUsers Method

Moving forward, we enhance our getUsers method, leveraging the Prisma Client for streamlined user retrieval:

// ...

getUsers = () => this.prisma.user.findMany({
select: {
id: true,
email: true,
age: true,
},
});

// ...

By leveraging Prisma, we’ve simplified our user retrieval process, making it more efficient and type-safe.

Updating the addUser Method

Now, let’s transform our addUser method into an asynchronous function, taking advantage of Prisma's capabilities:

// ...

addUser = async (user) => {
const { password, ...createdUser } = await this.prisma.user.create({
data: user,
});
return createdUser;
};

// ..

This modification ensures that adding a new user is an asynchronous operation, allowing for better handling of database interactions.

Updating the getUser Method

Lastly, let’s update our getUser method to use the Prisma Client for fetching a unique user based on their ID:

// ...

getUser = (id) => this.prisma.user.findUnique({
where: { id },
select: {
id: true,
email: true,
age: true,
},
});

// ...

By incorporating Prisma, we enhance the reliability and type safety of our user retrieval process.

With these updates, our Express backend now seamlessly integrates Prisma, providing a robust and efficient mechanism for interacting with the underlying database.

Updating our Controller

With the seamless integration of Prisma into our backend, our controller now undergoes a refinement to align with the capabilities of the Prisma Client. We no longer need the user.entities.js file, as Prisma handles our data modeling requirements.

Refining Controller Functions

Let’s enhance our UserController by removing the instantiation of the User class, as it's no longer needed. Additionally, we'll convert our functions to asynchronous to fully leverage Prisma's asynchronous nature. We'll also incorporate the use of our UserService:

class UserController {
constructor(userService) {
this.userService = userService;
}

createUser = async (req, res) => {
try {
const user = await this.userService.addUser(req.body);
return res.status(201).send(user);
} catch (error) {
return res.status(500).json({ error: 'Internal Server Error' });
}
};

getUsers = async (_, res) => {
try {
const users = await this.userService.getUsers();
return res.status(200).send(users);
} catch (error) {
return res.status(500).json({ error: 'Internal Server Error' });
}
};

getUser = async (req, res) => {
try {
const { id } = req.params;
const user = await this.userService.getUser(Number(id));
return res.status(200).send(user);
} catch (error) {
return res.status(500).json({ error: 'Internal Server Error' });
}
};
}

export default UserController;

These adjustments ensure that our controller seamlessly integrates with the enhanced data handling capabilities provided by Prisma. Error handling has also been incorporated to gracefully manage potential issues during user creation and retrieval.

Conclusion

In this tutorial, we’ve successfully integrated Prisma into our Node.js backend, augmenting our Express application with a robust and type-safe database layer. By following the step-by-step implementation, we’ve harnessed the power of Prisma to simplify database interactions, enhance reliability, and ensure type safety throughout our application.

Key Takeaways:

  1. Efficient Data Modeling: Prisma empowers us to define and model our data effortlessly, providing a clear and concise representation of our database schema.
  2. Seamless Database Interactions: With the Prisma Client, our backend seamlessly interacts with the database, resulting in concise and readable code for database operations.
  3. Type-Safe Operations: Prisma’s type-safe queries and auto-generated client code eliminate runtime errors, ensuring that our application is robust and reliable.

What’s Next?

As you continue to refine and expand your Node.js backend, consider exploring advanced features of Prisma, such as migrations, transactions, and relationships. Additionally, delve into performance optimizations and explore Prisma’s compatibility with various databases.

For further insights into testing strategies, don’t forget to check out my tutorial on From Unit to End-to-End Testing with Jest and Supertest, where we explore comprehensive testing approaches to ensure the resilience of your backend.

Keep Building, Keep Scaling

Keep Building, Keep Scaling

If you have questions or want to share your experiences, feel free to connect in the comments section below. Happy coding!

--

--

Camilo Salazar

Driven to share powerful ideas and experiences that ignite inspiration, deepen knowledge, and empower others