Prisma ORM with MongoDB in Nextjs

Chukwudi Nweze is a Frontend Engineer with a strong focus on web performance optimization and user experience. He writes about strategies that make modern web apps faster and more efficient.
Frontend development has undergone significant changes over the years, transitioning from simple DOM manipulation with jQuery to the implementation of full-stack applications using modern frameworks such as Next.js. While it empowers frontend developers with the capability to create full-stack applications, it also presents its fair share of challenges.
Developers now find themselves not only focused on designing the user interface but also dealing with the complexities of database manipulation, which might include the need to learn query languages. This can be quite overwhelming, especially for beginners.
ORM
This is where Object Relational Mapper (ORM) comes in handy. ORM is designed to make database interactions more developer-friendly, particularly for frontend developers, by using an intuitive and object-oriented approach.
Prisma
Prisma is an open-source, next-generation, and database-agnostic ORM. It supports various databases, including popular options like MongoDB, Postgres, MySQL, SQLite, and many more. This flexibility enables you to switch databases with minimal code adjustments.
Our Project in Focus
In this tutorial, we won't dive into a super complicated project, but we also won't stick to the most basic "Hello World" example. Instead, we'll focus on creating a to-do list app with CRUD (Create, Read, Update, and Delete) operations. The goal is not to write the most optimized code but to provide code that developers at all levels, including beginners, mid-level, and experienced developers, will understand. The primary goal is to provide a beginner-friendly learning experience for all.
Prerequisites
To successfully follow and complete this tutorial, you should have:
A basic understanding of TypeScript and Next.js
An active MongoDB Atlas account.
If you're using Visual Studio Code (VSCode), I recommend installing the Prisma VSCode Extension.
Basic Knowledge of Tailwind will be nice but not required
Setting up the app
To save time and get to the main purpose of this tutorial, we’ll use a simple starter code I have already prepared. The starter code includes TailwindCSS, Lucid React icons, React-hot-toast, as the dependencies. It also has the following components: Dashboard.tsx, input.tsx, todo-list.tsx, todo-item.tsx, and format-date.ts. You don't have to worry; we will explore the starter project structure, functionalities, and components.
https://github.com/chukwudinweze/todo-app-prisma-mongodb-starter-code.git
# cd into the directory
cd todo-app-prisma-mongodb-starter-code.git
# install dependencies
yarn install
or
npm install
Once installed, our project directory should look something like this:
📦app
┣ 📂(root)
┃ ┣ 📂_components
┃ ┃ ┗ 📜dashboard.tsx
┃ ┗ 📜page.tsx
┣ 📜favicon.ico
┣ 📜globals.css
┗ 📜layout.tsx
📦components
┣ 📂providers
┃ ┗ 📜toaster-provider.tsx
┣ 📜input.tsx
┣ 📜todo-item.tsx
┗ 📜todo-list.tsx
📦lib
┗ 📜format-date.ts
📦node_modules
📦public
┣ 📜next.svg
┗ 📜vercel.svg
┣ 📜.eslintrc.json
┣ 📜.gitignore
┣ 📜next-env.d.ts
┣ 📜next.config.js
┣ 📜package-lock.json
┣ 📜package.json
┣ 📜postcss.config.js
┣ 📜README.md
┣ 📜tailwind.config.ts
┣ 📜tsconfig.json
┗ 📜yarn.lock
To launch the application, run the following command, depending on the package manager you used to install the dependencies.
If you used yarn as your package manager, run:
yarn dev
If you used npm as your package manager, run:
npm run dev

Open http://localhost:3000 in your browser to see the to-do app running. You can also view the starter code hosted on Vercel. We're currently rendering a list of dummy to-do items.

Connecting Prisma and Mongodb in NextJs
The next steps involve installing Prisma and MongoDB, setting up a Prisma schema, and creating route handlers to manage and store our todo items in MongoDB.
Installing prisma:
yarn add -D prisma
or
npm install -D prisma
Initiate Prisma:
npx prisma init

npx prisma init command initiates Prisma in our project. It generates Prisma/schema.prisma and .env file in the project's root. Prisma/schema.prisma is where we will define our todo data structure. .env file is where we will specify our MongoDB connection url.
Our directory structure should now look something like this:
📦app
┣ 📂(root)
┃ ┣ 📂_components
┃ ┃ ┗ 📜dashboard.tsx
┃ ┗ 📜page.tsx
┣ 📜favicon.ico
┣ 📜globals.css
┗ 📜layout.tsx
📦components
┣ 📂providers
┃ ┗ 📜toaster-provider.tsx
┣ 📜input.tsx
┣ 📜todo-item.tsx
┗ 📜todo-list.tsx
📦prisma
┗ 📜schema.prisma
📦public
┣ 📜next.svg
┗ 📜vercel.svg
┣ 📜.env
┣ 📜.eslintrc.json
┣ 📜.gitignore
┣ 📜next-env.d.ts
┣ 📜next.config.js
┣ 📜package-lock.json
┣ 📜package.json
┣ 📜postcss.config.js
┣ 📜README.md
┣ 📜tailwind.config.ts
┣ 📜tsconfig.json
┣ 📜yarn-error.log
┗ 📜yarn.lock
Installing MongoDB:
#run
yarn add mongodb
or
#run
npm install mongodb
Generate a MongoDB Connection url:
Sign in or register. Select or create an organization. Create a project named todo-app-prisma-mongodb. On the left sidebar, click on Database, then click on Build Database and choose Free Cluster. Add your IP address and click Finish.
Now, click on Overview in the sidebar, and then click on Connect. Select MongoDB for Visual Studio Code, and copy the url. Return to our project's .env file, replace the DATABASE_URL with the copied url as shown below.

Prisma client:
The next step is to install Prisma Client. Once we define our schema and run Prisma generate in the next step below, Prisma Client will generate types and methods that we can use to query the database.
Install prisma client:
#run
yarn add @prisma/client
or
#run
npm install @prisma/client
Create a new directory named lib at the root of the application. Inside lib, create db.ts file and paste the code below.
// lib/db.ts
import { PrismaClient } from "@prisma/client";
export const db = new PrismaClient();
In development, hot reloading creats a new PrismaClient instance each time we save our code. This, in turn, results in multiple connection pools which will likely crash our app.
To avoid this, update db.ts file as shown in the code below.
// lib/db.ts
import { PrismaClient } from "@prisma/client";
declare global {
var prisma: PrismaClient | undefined;
}
export const db = globalThis.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") globalThis.prisma = db;
The provided code connects our todo app to the database while also ensuring that only one instance of PrismaClient is created across our application.
Prisma schema
Now that we've established a connection between our app and the database, the next step is to update prisma.schema file.
Open prisma.schema file within the prisma directory and make the following changes as shown below.
// prisma/prisma.schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
relationMode = "prisma"
}
model Todo {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isCompleted Boolean? @default(false)
}
datasource db specify the data sources Prisma should connect to while Todo model defines the data structure of our todo items. Each todo item consits of id, title, createdAt, updatedAt and an optional isCompleted field.
Generate Prisma Client
With the schema defined as shown above, our next step is to generate prisma client. As earlier stated, this process automatically generates types and methods that we will use to query the database.
#run
npx prisma generate

Congratulations on reaching this point. Finally, we need to push model Todo to the database. To achieve this, run the command below.
npx prisma db push

This will create Todo collection in our in the database.

CRUD Operations with Prisma and MongoDB in Next.js
To create and save our todo in the database, navigate to components/input.tsx and update createTodo function as shown below.
//components/input
const createTodo = async (e: React.FormEvent) => {
e.preventDefault();
if (!todoTitle) {
alert("Title required");
return;
}
setIsLoading(true);
try {
const apiUrl = "/api/todo/create";
const requestData = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ todoTitle }),
};
const response = await fetch(apiUrl, requestData);
if (!response.ok) {
throw new Error(
`Failed to post title: ${response.status} - ${response.statusText}`
);
}
setTodoTitle("");
toast.success("Todo created");
} catch (error) {
console.log(error);
toast.error("something went wrong");
} finally {
setIsLoading(false);
}
};
createTodo sends a POST request with the todoTitle to the route handler app/api/todo/create/route.ts
Now, Create the route handler app/api/todo/create/route.ts that will handle the POST request from components/input.
Your app directory should look like this:
📦app
┣ 📂(root)
┃ ┣ 📂_components
┃ ┃ ┗ 📜dashboard.tsx
┃ ┗ 📜page.tsx
┣ 📂api
┃ ┗ 📂todo
┃ ┃ ┗ 📂create
┃ ┃ ┃ ┗ 📜route.ts
┣ 📜favicon.ico
┣ 📜globals.css
┗ 📜layout.tsx
Paste the code below inside the route handler app/api/todo/create/route.ts.
// app/api/todo/create/route.ts
import { db } from "@/lib/db";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
try {
// detsrtucture todoTitle from the incoming request
const { todoTitle } = await req.json();
if (!todoTitle) {
return new NextResponse("Title required", { status: 400 });
}
// Create and save todo on the database
const todo = await db.todo.create({
data: {
title: todoTitle,
},
});
return NextResponse.json(todo, { status: 200 }); // Respond with the created todo
} catch (error) {
console.log("[POST TODO]", error);
return new NextResponse("Internal Server Error", { status: 500 }); // Handle errors
}
}
We destructured todoTitle from req.json(). We check for todotitle validity and then proceed to create and store our new todo in the database.
Now, navigate to our app on the browser and create a new todo.

We should see the new todo inside todo-app-prisma-mongodb collection if we check our MongoDB atlas.

Fetching data with prisma in nextjs
Now that we have created and saved our first todo in the database, we need to fetch our todo from the database so we can display it on the browser.
Create another route handler app/api/todo/route.ts and paste the code below.
// app/api/todo/route.ts
import { db } from "@/lib/db";
import { NextResponse } from "next/server";
export async function GET() {
try {
//fetch todos from the db
const todos = await db.todo.findMany({
orderBy: {
createdAt: "desc",
},
});
// respond with the todos
return NextResponse.json(todos, { status: 200 });
} catch (error) {
console.log("[GET TODO]", error);
// Handle errors
return new NextResponse("Internal Server Error", { status: 500 });
}
}
Rendering todo Items:
Navigate to components/todo-lists and update the useffect hook as shown below. We are sending a GET request to the route handler app/api/todo/route.ts
// components/todo-lists
useEffect(() => {
//fetch todos
const fetchTodos = async () => {
try {
const response = await fetch(`/api/todo`, {
next: { revalidate: 3600 },
});
if (!response.ok) {
throw new Error(
`Failed to fetch items: ${response.status}`
);
}
const data = await response.json();
setTodos(data);
} catch (error: any) {
console.error(`Error fetching items: ${error.message}`);
toast.error("unable to fetch todos at this time");
}
};
// call fetch fetchTodos
fetchTodos();
}, []);
We should be able to see our todos displayed.

Updating our todo items:
The next step is to update any of our completed todo item. This is achieved by modifying the isCompleted field for the respective todo in the database.
Create a dynamic route handler app/api/todo/[todoId]/update/routes.ts and paste the code below.
// app/api/todo/[todoId]/update/routes.ts
import { db } from "@/lib/db";
import { NextResponse } from "next/server";
export async function PATCH(
req: Request,
{ params }: { params: { todoId: string } }
) {
try {
if (!params.todoId) {
return new NextResponse("Not found", { status: 404 });
}
// update todo item whose id matched params.todoId
const updatedTodo = await db.todo.update({
where: {
id: todoId,
},
data: {
isCompleted: true,
},
});
// Respond with the updated todo
return NextResponse.json(updatedTodo, { status: 200 });
} catch (error) {
console.log("[UPDATE TODO]", error);
// Handle errors
return new NextResponse("Internal Server Error", { status: 500 });
}
}
Navigate to components/todo-item.tsx and update the completTodo function as shown below.
// components/todo-item.tsx
const completeTodo = async () => {
try {
const apiUrl = `/api/todo/${id}/update`;
const requestData = {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
};
const response = await fetch(apiUrl, requestData);
if (!response.ok) {
throw new Error(
`Failed to post title: ${response.status} - ${response.statusText}`
);
}
toast.success("Todo updated");
// refresh page on successful request
window.location.reload();
} catch (error) {
console.log(error);
toast.error("something went wrong");
} finally {
setIsLoading(false);
}
};
completTodo is now an async function, and we are sending PATCH request to the dynamic route handler app/api/todo/[todoId]/update/routes.ts.
We can now update the respective todo item whose id matches params.todoId.
Navigate to the browser, and click on the green checkmark button to update a todo.

Editing our todos
Similar to how we updated our todo items, we should also be able to edit our todo title. To achieve this, create another dynamic route handler app/api/todo/[todoId]/edit/routes.ts and paste the code below
// app/api/todo/[todoId]/edit/routes.ts
import { db } from "@/lib/db";
import { NextResponse } from "next/server";
export async function PATCH(
req: Request,
{ params }: { params: { todoId: string } }
) {
try {
if (!params.todoId) {
return new NextResponse("Not found", { status: 404 });
}
const { todoTitle } = await req.json();
// edit todo title
const updatedTodo = await db.todo.update({
where: {
id: params.todoId,
},
data: {
title: todoTitle,
},
});
// Respond with the updated todo
return NextResponse.json(updatedTodo, { status: 200 });
} catch (error) {
console.log("[UPDATE TODO]", error);
// Handle errors
return new NextResponse("Internal Server Error", { status: 500 });
}
}
Navigate to components/input.txs and update createTodo function as shown below.
// components/input.txs
const createTodo = async (e: React.FormEvent) => {
e.preventDefault();
if (!todoTitle) {
alert("Title required");
return;
}
setIsLoading(true);
try {
const apiUrl = isEditing
? `/api/todo/${itemToEdit.id}/edit`
: "/api/todo/create";
const reqData = isEditing
? { todoTitle }
: { todoTitle, id: itemToEdit.id };
const reqMethod = isEditing ? "PATCH" : "POST";
const requestData = {
method: reqMethod,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(reqData),
};
const response = await fetch(apiUrl, requestData);
if (!response.ok) {
throw new Error(
`Failed to ${isEditing ? "Edit" : "Create"} Todo: ${
response.statusText
}`
);
}
setTodoTitle("");
toast.success(`${isEditing ? "Todo edited" : "Todo create"}`);
// refresh page on successful request
window.location.reload();
} catch (error) {
console.log(error);
toast.error("something went wrong");
} finally {
setIsLoading(false);
isEditing = false;
}
};
Navigate to the browser, you should be able to edit a todo item

Deleting todo item
We have successfully implemented create, update, and render operations for our todo app. The last step in our CRUD operation with prisma and mongodb in our nextjs todo app is for us to be able to delete todos. To achieve this, create another dynamic route handler app/api/todo/[todoId]/delete/routes.ts and paste the code below
// app/api/todo/[todoId]/delete/routes.ts
import { db } from "@/lib/db";
import { NextResponse } from "next/server";
export async function DELETE(
req: Request,
{ params }: { params: { todoId: string } }
) {
try {
if (!params.todoId) {
return new NextResponse("Not found", { status: 404 });
}
// delete todo from the db
const deletedTodo = await db.todo.delete({
where: {
id: params.todoId,
},
});
// Respond with the deleted todo
return NextResponse.json(deletedTodo, { status: 200 });
} catch (error) {
console.log("[DELETE TODO]", error);
// Handle errors
return new NextResponse("Internal Server Error", { status: 500 });
}
}
Navigate to components/todo-item.tsx and update the deleteTask function
//components/todo-item.tsx
const deleteTask = async () => {
alert(`delete ${title}?`);
try {
const apiUrl = `/api/todo/${id}/delete`;
const requestData = {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
};
const response = await fetch(apiUrl, requestData);
if (!response.ok) {
throw new Error(
`Failed to delete ${title} - ${response.statusText}`
);
}
toast.success("Todo deleted");
// refresh page on successful request
window.location.reload();
} catch (error) {
console.log(error);
toast.error("something went wrong");
} finally {
setIsLoading(false);
}
};
Navigate to the browser and click on the delete button, and yes, we can now delete our todos!

Conclusion
We were able to cover topics ranging from Prisma installation and integration with MongoDB in Next.js. We also learned how to create a basic Prisma schema, generate a MongoDB connection url, connect it to Prisma, and build a CRUD application.
If you're a beginner, I'm glad that you've successfully followed through to this point.
I'd like to offer you a challenge:
Allow only authenticated users to perform create, update, edit, and delete todos.
Ensure that only the todos created by the user are rendered.
Additionally, use Prisma Studio to view the mirror of your database.
Here is the complete code for our Todo app. You can also view it on Vercel.
Don't hesitate to reach out or leave a comment if you get stuck.

