Blog Post CoverEngineering

How to Build a Fullstack App with Next.js, Prisma & MongoDB

Learn how to build a fullstack app with Next.js, Prisma, and MongoDB. Explore API routes, advanced Prisma queries, filtering, pagination, and sorting.

Blog-Post-Author

Amal

Created: September 11, 2024

Updated: September 18, 2024


Our mission is to make the Internet a safer place, and the new login standard passkeys provides a superior solution to achieve that. That's why we want to help you understand passkeys and its characteristics better.

1. Introduction: Next.js & Prisma#

Building professional web applications can be quite challenging, especially when managing the front-end, back-end, and database interactions. Next.js and Prisma have emerged as two popular tools that simplify this process, allowing developers to focus more on building features rather than worrying about the infrastructure.

Next.js is a framework based on React. It’s ideal for fullstack applications, providing all the tools needed to create both the frontend and backend in one framework. On the other hand, Prisma is a next-generation Object Relational Mapper (ORM) designed to streamline database access and management. Combining Next.js and Prisma brings simplicity, speed, and scalability.

In this guide, we’ll explore why developers increasingly choose Prisma and Next.js, and how you can use them to build a performant fullstack web app.

By the end of this blog, you'll have answers to the following key questions:

  1. How to set up a Next.js project using Prisma as the ORM?

  2. How to implement API Routes and using Prisma with MongoDB in a Next.js app?

  3. What are Prisma’s advanced querying capabilities?

2. What is an ORM?#

An ORM acts as a bridge between your application and the database, allowing you to work with database records using code instead of raw queries. By translating database tables into objects and simplifying data retrieval, ORMs like Prisma help developers focus on building application features rather than writing complex queries.

The benefits of using an ORM include:

  • Simplified database interactions: No need for complex queries.

  • Improved code readability and maintainability: Work with familiar object structures.

  • Portability: Easily switch between databases without major code changes.

Substack Icon

Subscribe to our Passkeys Substack for the latest news, insights and strategies.

Subscribe

3. What is Prisma?#

Prisma is an open-source Node.js and TypeScript ORM that simplifies database interactions for developers working with JavaScript and TypeScript.

It allows you to define your database schema in TypeScript, and it supports multiple databases such as MongoDB, PostgreSQL, MySQL, and many more. It automates the generation of queries and migrations, making database management easier for developers.

Slack Icon

Become part of our Passkeys Community for updates and support.

Join

3.1 What are the Core Features of Prisma?#

These are the main features of Prisma:

  • Type Safety for Database Queries: Prisma ensures that your database queries are fully type safe, reducing potential errors.

  • Auto-generation of Queries: Prisma generates queries automatically based on the models you define. This means there is no need to manually write query logic for common tasks like fetching, updating, or deleting data.

  • Auto-generation of TypeScript Types: Prisma automatically generates TypeScript types based on your database schema, making it easy to work with strongly typed data throughout your application.

  • Multi-database Support: Prisma supports a variety of databases, including MongoDB, PostgreSQL, and more, giving you the flexibility to work with the database of your choice.

  • Migrations: Prisma simplifies database schema changes with easy-to-use migrations, allowing you to evolve your database schema as your project grows.

3.2 Why Choose Prisma?#

Prisma stands out among popular ORMs like TypeORM and Drizzle ORM for:

  • its strong focus on type safety

  • developer experience

  • automatic query generation

Unlike TypeORM, which requires more manual handling for complex queries, and Drizzle ORM, which is lightweight but lacks some of Prisma’s tooling, Prisma’s intuitive syntax, TypeScript support, and tools like Prisma Studio enhance the development experience by offering an interface to explore and manipulate the data with a simple command.

npx prisma studio

Prisma Studio Interface

3.3 Why Prisma and Next.js Are a Great Fit?#

Prisma and Next.js combined form an efficient and developer-friendly full-stack solution. Prisma simplifies database management with type-safe queries, integrating seamlessly with Next.js’ TypeScript-first approach. Together, they enable smooth development by leveraging Next.js for both frontend and backend, while Prisma handles database access. This makes building full-stack applications with server-side rendering and secure database operations more reliable.

Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

4. Build a Sample App with Next.js, Prisma, and MongoDB#

In this section, we’ll walk you through building a user management app using Next.js, Prisma, and MongoDB. This step-by-step guide will show you how to integrate Prisma for database management and create API routes to demonstrate Prisma’s ORM capabilities. The app will feature a form for adding new users and display a list of existing users, providing a hands-on example of fullstack development.

User Management App

4.1 Prerequisites#

This tutorial assumes you are familiar with Next.js, TypeScript, Prisma, and Docker. Moreover, you need Node, NPM, and Docker installed on your machine.

4.2 Setting Up Docker Desktop#

  1. Check System Requirements: Ensure your system meets Docker's minimum requirements (OS version, RAM, CPU, etc.).

  2. Download Docker Desktop: Visit the Docker Desktop website and download the installer for your operating system.

  3. Install Docker Desktop: Run the installer file you downloaded and follow the installation instructions presented by the setup wizard.

  4. Start Docker Desktop: Launch Docker Desktop, and it will start running in the background.

  5. Verify Installation: Open your terminal and run the following command to check if Docker is installed correctly:

docker --version

4.3 Repository Structure#

Let's explore the structure of our repository full Nextjs & Prisma GitHub repository.

. │── app β”‚ β”œβ”€β”€ api β”‚ β”‚ β”œβ”€β”€ createUser β”‚ β”‚ β”‚ └── route.ts β”‚ β”‚ β”œβ”€β”€ getListUsers β”‚ β”‚ └── route.ts β”‚ β”œβ”€β”€ layout.tsx β”‚ └── page.tsx │── node_modules │── prisma β”‚ └── schema.prisma │── .env │── docker-compose.yml │── Dockerfile │── package.json │── README.md

Here's a brief explanation of the relevant files and directories:

  1. app/: Contains the Next.js application logic, including the API routes for user management.

    • api/: This directory holds the API routes:

      • createUser/route.ts: Handles the POST request to add a new user to the database using Prisma.

      • getListUsers/route.ts: Handles the GET request to retrieve the list of users from the MongoDB database via Prisma.

  2. prisma/: Includes the Prisma schema for managing database models.

    • schema.prisma: This file defines the Prisma schema, where you set up models and the database connection. In this case, it configures Prisma to work with MongoDB and defines the User model.
  3. docker-compose.yml : Configuration for running the app and database services using Docker.

  4. Dockerfile: Instructions for building the Next.js application container.

StateOfPasskeys Icon

Want to find out how many people can use passkeys?

View Adoption Data

4.4 Setting Up a Next.js Project with Prisma#

4.4.1 Set Up the Next.js Project#

First, we need to create a Next.js project using this command

npx create-next-app@latest nextjs-prisma-app cd nextjs-prisma-app

4.4.2 Install Prisma#

Next, we need to install Prisma using the following command:

npm install @prisma/client npm install -D prisma
Debugger Icon

Want to experiment with passkey flows? Try our Passkeys Debugger.

Try for Free

4.5 Initializing Prisma Schema#

Now, you need to initialize Prisma in your project using this command:

npx prisma init

This will create a prisma/ directory with a schema.prisma file and an . env file and also will guide you through the next steps.

Prisma Initialization

4.6 Configuring Prisma for MongoDB#

In the containerized environment, the MongoDB service runs in a Docker container named mongodb. Therefore, the DATABASE_URL is updated to reflect that MongoDB is accessible at mongodb:27017 inside the Docker network.

This environment variable is defined in the docker-compose.yml file:

docker-compose.yml
DATABASE_URL=mongodb://mongodb:27017/users_management

We need also to connect Prisma to our database. We can do this by editing the schema.prisma file and adding the following code:

prisma/schema.prisma
datasource db { provider = "mongodb" url = env("DATABASE_URL") }

4.7 Defining a Model#

In Prisma, a model represents a collection in your database (for MongoDB, this means a collection, and for relational databases, it's a table). Prisma uses these models to automatically generate the underlying database schema and provides a way to interact with the data using the Prisma Client.

We can create a model, by adding the following code to the schema.prisma file. This defines a simple User model with id, name, and email fields which will be mapped to a MongoDB collection named User. Each instance of this model represents a document inside that collection.

prisma/schema.prisma
model User { id String @id @default(auto()) @map("_id") @db.ObjectId name String email String @unique }
  • id String @id @default(auto()) @map("_id") @db.ObjectId: This is the primary identifier for each document in the collection. Every model in Prisma must have an id field that uniquely identifies each record.

    • String: The data type of the id field is String. For MongoDB, this field will hold the ObjectId.

    • @id: This annotation tells Prisma that this field is the primary key.

    • @default(auto()): This specifies that the id should be automatically generated by Prisma. In MongoDB, the default for the _id field is an ObjectId which is automatically generated by the database.

    • @map("_id"): MongoDB uses the _id field as the primary key by default. The @map("_id") directive tells Prisma to map the id field in the Prisma model to the MongoDB _id field. This way, Prisma knows that the id field in the model corresponds to _id in the database.

    • @db.ObjectId: This indicates that in MongoDB, the id field is of the ObjectId type. Although Prisma uses the String type, it understands that the underlying database will use MongoDB's ObjectId.

  • name String: This defines a name field of type String. It is a simple field where each user can have a name. Prisma automatically maps this field to a field in the MongoDB collection.

  • email String @unique: This defines an email field, also of type String. It represents the user's email address.

    • @unique: This directive ensures that the email field is unique across all documents in the collection. Prisma enforces this uniqueness constraint when inserting or updating documents.

4.8 Generate Prisma Client#

Prisma offers a client that allows seamless interaction with the database. You can generate this client by running the following command:

npx prisma generate

4.9 Creating an API Routes#

In this section, we create two API routes to interact with our MongoDB database via Prisma. These routes will allow fetching and creating users.

4.9.1 Fetching Users API Route#

This API route retrieves a list of all users from the database using the Prisma method findMany(). If the DATABASE_URL is not set, it returns a 500 error. If the request is successful, it returns a JSON array of users.

app/api/getListUsers/route.ts
import { NextRequest, NextResponse } from "next/server"; import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); export async function GET(req: NextRequest) { if (!process.env.DATABASE_URL) { return NextResponse.json( { error: "DATABASE_URL is not set" }, { status: 500 } ); } try { const users = await prisma.user.findMany(); return NextResponse.json(users ?? []); } catch (error) { return NextResponse.json( { error: "Failed to fetch users" }, { status: 500 } ); } }

4.9.2 Adding a New User API Route#

This API route allows adding a new user using the Prisma create() method by sending a POST request with name and email. It validates the request and saves the user to the database.

app/api/createUser/route.ts
import { NextRequest, NextResponse } from "next/server"; import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); export async function POST(req: NextRequest) { if (!process.env.DATABASE_URL) { return NextResponse.json( { error: "DATABASE_URL is not set" }, { status: 500 } ); } try { const body = await req.json(); const { name, email } = body; if (!name || !email) { return NextResponse.json( { error: "Missing required fields" }, { status: 400 } ); } const user = await prisma.user.create({ data: { name, email, }, }); return NextResponse.json(user, { status: 201 }); } catch (error) { console.error("Error during user creation:", error); return NextResponse.json( { error: "User creation failed" }, { status: 400 } ); } }

4.10 Creating a Component to Interact with the APIs#

Now, we create a basic component that interacts with the APIs. It provides a form to add new users and displays the list of existing users.

This component uses React hooks (useState, useEffect) to manage user state. It fetches the list of users from the API and updates the UI when a new user is added via a form.

app/page.tsx
"use client"; import { useState, useEffect } from "react"; type User = { id: string; name: string; email: string; }; export default function Home() { const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [users, setUsers] = useState<User[]>([]); const fetchUsers = async () => { const res = await fetch("/api/getListUsers"); const data = await res.json(); setUsers(data); }; const addUser = async (e: React.FormEvent) => { e.preventDefault(); const res = await fetch("/api/createUser", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ name, email }), }); if (res.ok) { setName(""); setEmail(""); fetchUsers(); } }; useEffect(() => { fetchUsers(); }, []); return ( <div className="min-h-screen flex flex-col items-center justify-center bg-gray-100"> <h1 className="text-3xl font-bold mb-6">User Management</h1> <form onSubmit={addUser} className="bg-white p-6 rounded-lg shadow-md flex flex-col items-center space-y-4" > <input type="text" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} className="p-2 border border-gray-300 rounded w-64" /> <input type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} className="p-2 border border-gray-300 rounded w-64" /> <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" > Add User </button> </form> <ul className="mt-6 bg-white p-6 rounded-lg shadow-md w-96"> <h3 className="text-center font-bold">List of Users</h3> {Array.isArray(users) && users.length > 0 ? ( users.map((user) => ( <li key={user.id} className="border-b border-gray-200 py-2 text-center" > {user.name} - {user.email} </li> )) ) : ( <li className="text-center">No users found</li> )} </ul> </div> ); }

4.11 Running the Application#

This user management application uses two Docker containers. The first container runs MongoDB serving as the database for user data. It stores persistent data in a mounted volume and is configured to run with a Replica Set (Prisma requirement). The second container hosts the Next.js application, which connects to MongoDB through an internal network. This container handles the app's logic, rendering the frontend, and managing API requests. Both containers are connected via a shared Docker network, ensuring smooth communication between the app and the database.

4.11.1. Start the Containers#

To start the MongoDB and Next.js containers, run the following command:

docker-compose up --build

This will build and start both the MongoDB container on port 27017 and the Next.js app on port 3000. Also, it will generate the Prisma Client inside the app container.

4.11.2. Initialize MongoDB Replica Set#

Once the containers are running, you must initialize the MongoDB Replica Set. In a new terminal, run these commands:

docker exec -it mongodb mongosh

MongoDB Shell

Now you need to initiate the Replica Set by running the following command inside the MongoDB shell:

rs.initiate()

Initiate the Replica Set You can verify the Replica Set status

rs.status()

Check the Replica Set Status

After initializing the MongoDB Replica Set, you can access the Next.js app in your browser at: http://localhost:3000 User Management App Interface

5. Advanced Prisma Features in Next.js#

Prisma offers flexible query capabilities, making it easy to implement features like filtering, pagination, and sorting in your Next.js applications. Let's explore some of these advanced features.

5.1 Filtering#

Filtering allows you to retrieve only specific records from the database based on certain conditions. Prisma provides an easy way to add filters to your queries using the where clause.

  • Example of Filtering Users by Name

In this example, users whose names contain the search query are returned. You can customize the filtering logic to match exact names, case-insensitive strings, or even other fields.

const nameQuery = req.nextUrl.searchParams.get("name"); const users = await prisma.user.findMany({ where: { name: { contains: nameQuery } }});

5.2 Pagination#

Pagination is important when dealing with large datasets, allowing you to load results in chunks. Prisma makes pagination easy using the skip and take options.

  • Example of Pagination Users

In this example, users are returned in batches of 10. You can adjust the pageSize to control how many users are returned per page, and the page parameter to specify which page of results to return.

Here’s how you can implement pagination to retrieve users in batches:

const page = Number(req.nextUrl.searchParams.get("page") || "1"); const pageSize = 10; const users = await prisma.user.findMany({ skip: (page - 1) * pageSize, take: pageSize, });

5.3 Sorting#

Sorting helps you order your data based on specific fields (e.g., name, email, created date). Prisma allows you to easily sort results using the orderBy option.

  • Example of Sorting Users by Name

This code returns users sorted alphabetically by their names in ascending order.

const users = await prisma.user.findMany({ orderBy: { name: 'asc' } });

6. Conclusion#

Combining Next.js and Prisma creates an efficient stack for building full-stack web applications. Prisma simplifies the complexities of database management, offering a type-safe and intuitive way to interact with databases like MongoDB, while Next.js provides the perfect framework for building scalable, server-side rendered, and API-driven applications. By using advanced Prisma features like filtering, pagination, and sorting, you can enhance your application's functionality and improve user experience.

7. Resources#

Here are some valuable resources:

Share this article


LinkedInTwitterFacebook

Enjoyed this read?

🀝 Join our Passkeys Community

Share passkeys implementation tips and get support to free the world from passwords.

πŸš€ Subscribe to Substack

Get the latest news, strategies, and insights about passkeys sent straight to your inbox.


We provide UI components, SDKs and guides to help you add passkeys to your app in <1 hour

Start for free