Authentication and DB access with Next, Prisma, and MongoDB
Tags: webdev, javascript, nextjs, react
Published: 2022-06-22 23:38:04

by [Miracle Onyenma](https://blog.openreplay.com/authors/miracle-onyenma)


Building modern full-stack applications has gotten easier for frontend developers with new technologies and tools. With these, we can build frontend applications with server-side capabilities with frameworks like Next.js. We can also abstract the complex details of interfacing with a database using an ORM like Prisma. In this tutorial, we'll learn how to build a full-stack application with authentication and CRUD functionality using Next.js, Prisma & MongoDB. We'll explain these technologies and related concepts and see how we can put these technologies together to build an application.

## Prisma

[Prisma](https://www.prisma.io/docs/concepts/overview/what-is-prisma) is a next-generation object-relational mapper **(ORM)** that can be used to access a database in Node.js and TypeScript applications. We'll use Prisma to interact with our MongoDB database to CREATE, READ, UPDATE and DELETE notes, create users in our database, and define the schema for users and notes.

Cool, but what's an ORM, and why do we need Prisma to access our database? [Object Relational Mapping](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) is a technique used to access and manage relational databases using an object-oriented programming language (like JavaScript). Prisma ORM works for MongoDB too, which is the document-based database that we'll be using in the article. ORMs let you model your application data by mapping *classes* to *tables* in the database, and instances of classes are *mapped* to rows in the tables. In Prisma, though, which takes a somewhat different approach from other traditional ORMs, the application models are defined in your [Prisma schema](https://www.prisma.io/docs/concepts/components/prisma-schema). 

Next, let's take a look at MongoDB.

## MongoDB

According to [Guru99](https://www.guru99.com/what-is-mongodb.html):

> MongoDB is a document-oriented NoSQL database used for high-volume data storage. Instead of using tables and rows as in the traditional relational databases, MongoDB uses collections and documents. Documents consist of key-value pairs, the basic unit of data in MongoDB. Collections contain sets of documents which is the equivalent of relational database tables. -->

[MongoDB](https://www.mongodb.com/) is a No-SQL database that stores data in documents in a JSON-like format. Collections are a set of documents equivalent to a table in a relational database. This is where we'll store our data. You can read a little more on [MongoDB at this post from Guru99](https://www.guru99.com/what-is-mongodb.html).

## Next.js

[Next.js](https://nextjs.org/) is an open-source React framework that makes it easier to build production-ready react applications by providing multiple features like server-side rendering and static-site generation, which renders the application on the server and hydrates (add the JavaScript functionality) on the client (browser). Next.js also includes features like **API Routes, File-system Routing, Fast Refresh, Built-in CSS Support, Meta tags,** and many more.

Let's dive into building our application.

## Prerequisites

To follow along and complete this tutorial, you'll need the following:

- Basic knowledge of JavaScript, React and Next.js
- Node.js installed; I'll be using v16.13.0
- A MongoDB Database. Make sure that your MongoDB deployment is configured with **replica sets** to be able to integrate with Prisma.  You can launch a free instance of MongoDB on [MongoDB Atlas](https://www.mongodb.com/cloud/atlas), which has replica set support out-of-the-box. You can also [convert standalone to replica set](https://docs.mongodb.com/manual/tutorial/convert-standalone-to-replica-set/) if you want to work with MongoDB locally.
- A Google account and access to the [Google Cloud Console](https://console.cloud.google.com/)
- If you are using VSCode, I recommend downloading the [Prisma VSCode Extension](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma)

## Setting up the Frontend

To save time and get into the core purpose of this tutorial, we'll use a simple starter with basic functionality and components built-in. This starter comes with TailwindCSS and the [heroicons](https://heroicons.com/) library. We'll explore the starter project structure, functionalities, and components before integrating with NextAuth, Prisma, and MongoDB. On your machine, clone the starter repo, and install dependencies:

```
git clone https://github.com/miracleonyenma/next-notes-app-starter.git

# cd into the directory
cd next-notes-app

# install dependencies
yarn
```

Once installed, our directory structure should look something like this:

```
📦next-notes-app
  ┣ 📂node_modules
  ┣ 📂components
  ┃ ┣ 📜Editor.js
  ┃ ┣ 📜NotesList.js
  ┃ ┗ 📜SiteHeader.js
  ┣ 📂layouts
  ┃ ┗ 📜default.js
  ┣ 📂modules
  ┃ ┣ 📜AppContext.js
  ┃ ┗ 📜RandomID.js
  ┣ 📂pages
  ┃ ┣ 📂api
  ┃ ┃ ┗ 📜hello.js
  ┃ ┣ 📂[user]
  ┃ ┃ ┗ 📜index.js
  ┃ ┣ 📜index.js
  ┃ ┗ 📜_app.js
  ┣ 📂public
  ┃ ┣ 📜favicon.ico
  ┃ ┗ 📜vercel.svg
  ┣ 📂styles
  ┃ ┣ 📜Editor.css
  ┃ ┣ 📜globals.css
  ┃ ┣ 📜Home.module.css
  ┃ ┣ 📜NoteList.css
  ┃ ┗ 📜SiteHeader.css
  ┣ 📜.eslintrc.json
  ┣ 📜.gitignore
  ┣ 📜next.config.js
  ┣ 📜package.json
  ┣ 📜postcss.config.js
  ┣ 📜README.md
  ┣ 📜tailwind.config.js
  ┗ 📜yarn.lock
```

To start the application, run `yarn dev` and the app should start running at `http://localhost:3000/`. 

![Running the Next.js app on the commandline](https://blog.openreplay.com/static/cb5431f15875d6d23331250e882098f7/94352/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653263505400_image.png)

Open in the browser to see the notes app running.

![Starter note app with basic functionality using context API state management](https://blog.openreplay.com/aa48ca69e4efe6482e040f40abcc4d83/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653263340930_next-notes-starter.gif)

You can view the [starter example hosted on Netlify](https://starter-next-notes-app.netlify.com/). Right now, we can create, update and delete notes. However, it's all on the client side. When we refresh the page, we lose all the data because the state is managed in our app using the [Context API](https://www.smashingmagazine.com/2020/01/introduction-react-context-api/)

### State management with Context API

Context API is a state management tool bundled with the React.js library itself. We typically need to pass data between components in any application. For example, to pass state from a parent component down to a child component, we would use `props`. However, passing state between sibling components and from a child component to its parent can become complex. That's where state management comes in, allowing any component anywhere in the application to have access to the application state. We could use Redux or Recoil, but we'll use the Context API. To see how our Context API works, open the `./modules/AppContext.js` file. We won't go in-depth about how Context API works in a Next.js application, but you can refer to this [GitHub example to see how to use the Context API in a Next.js application](https://github.com/vercel/next.js/blob/canary/examples/with-context-api/pages/index.js).

```javascript
    // ./modules/AppContext.js

    const { createContext, useState, useContext, useReducer } = require("react");

    // context data getter
    const NoteStateContext = createContext();
    const NotesStateContext = createContext();

    // context data setter
    const NoteDispatchContext = createContext();
    const NotesDispatchContext = createContext();

    // reducer function to modify state based on action types
    const notesReducer = (state, action) => {
      const { note, type } = action;
      if (type === "add") {...}
      if (type === "remove") {...}
      if (type === "edit") {...}
      return state;
    };

    // NoteProvider, which will wrap the application
    // providing all the nested state and dispatch context
    export const NoteProvider = ({ children }) => {
      // useState for note, to get and set a single note
      const [note, setNote] = useState({});

      // use Reducer for notes, to get all notes
      // and add, edit or remove a note from the array
      const [notes, setNotes] = useReducer(notesReducer, []);
      return (
        <NoteDispatchContext.Provider value={setNote}>
          <NoteStateContext.Provider value={note}>
            <NotesDispatchContext.Provider value={setNotes}>
              <NotesStateContext.Provider value={notes}>
                {children}
              </NotesStateContext.Provider>
            </NotesDispatchContext.Provider>
          </NoteStateContext.Provider>
        </NoteDispatchContext.Provider>
      );
    };

    // export state contexts
    export const useDispatchNote = () => useContext(NoteDispatchContext);
    export const useNote = () => useContext(NoteStateContext);
    export const useDispatchNotes = () => useContext(NotesDispatchContext);
    export const useNotes = () => useContext(NotesStateContext);
```

[View full code here](https://github.com/miracleonyenma/next-notes-app-starter/blob/starter-code/modules/AppContext.js)


Here, we have two basic types of context: the `state` context and the `dispatch` context created using the `createContext` hook.
The `state` context will act as a data getter, while the `dispatch` context will act as a setter.
To achieve this, within the `NoteProvider` function which wraps the `children` prop around the `DispatchContext` & `StateContext` providers, we use `useState` and `useReducer` hooks to create `notes` and `setNotes` for example.

```javascript
    const [notes, setNotes] = useReducer(notesReducer, []);
```

`setNotes` uses the `useReducer` hook and the `notesReducer` function to add, edit or delete a note from the `notes` array based on the `action.type` passed to the `setNotes` function and are passed into their context providers:

```javascript
    <NotesDispatchContext.Provider value={setNotes}>
      <NotesStateContext.Provider value={notes}>
```

At the end of the file, the context is exported using the `useContext` hook:

```javascript
    export const useDispatchNotes = () => useContext(NotesDispatchContext);
    export const useNotes = () => useContext(NotesStateContext);
```

For the entire application to have access to the context, we need to include it in our `./pages/app.js` file.

```javascript
    // ./pages/app.js

    import { NoteProvider } from "../modules/AppContext";
    import DefaultLayout from "../layouts/default";

    function MyApp({ Component, pageProps }) {
      return (
        <NoteProvider>
          <DefaultLayout>
            <Component {...pageProps} />
          </DefaultLayout>
        </NoteProvider>
      );
    }
    export default MyApp;
```

[View full code on Github](https://github.com/miracleonyenma/next-notes-app-starter/blob/starter-code/pages/_app.js)

Here, we wrap our application - `<Component {…pageProps} />` around the `NoteProvider` which we imported into the file `import { NoteProvider } from "../modules/AppContext";`
This makes any state within `NoteProvider`  available in all components in the application.

### The NoteList component

To access and modify this global state, we import the context we need from `./modules/AppContext.js` first. 

```javascript
// ./components/NotesList.js
import {  PencilAltIcon, TrashIcon, ExternalLinkIcon } from "@heroicons/react/solid";
import { useNote, useDispatchNote, useNotes, useDispatchNotes } from "../modules/AppContext";

const NotesList = ({ showEditor }) => {
  // this is where we assign the context to constants
  // which we will use to read and modify our global state
  const notes = useNotes();
  const setNotes = useDispatchNotes();
  const currentNote = useNote();
  const setCurrentNote = useDispatchNote();

  // function to edit note by setting it to the currentNote state
  // and adding the "edit" action
  // which will then be read by the <Editor /> component
  const editNote = (note) => {
    note.action = "edit";
    setCurrentNote(note);
  };

  // function to delete note by using the setNotes Dispatch notes function
  const deleteNote = (note) => {
    let confirmDelete = confirm("Do you really want to delete this note?");
    confirmDelete ? setNotes({ note, type: "remove" }) : null;
  };

  return (
    <div className="notes">
      {notes.length > 0 ? (
        <ul className="note-list">
          {notes.map((note) => (
            <li key={note.id} className="note-item">
              <article className="note">
                <header className="note-header">
                  <h2 className="text-2xl">{note.title}</h2>
                </header>
                <main className=" px-4">
                  <p className="">{note.body}</p>
                </main>
                <footer className="note-footer">
                  <ul className="options">
                    <li onClick={() => editNote(note)} className="option">
                      <button className="cta cta-w-icon">
                        <PencilAltIcon className="icon" />
                        <span className="">Edit</span>
                      </button>
                    </li>
                    <li className="option">
                      <button className="cta cta-w-icon">
                        <ExternalLinkIcon className="icon" />
                        <span className="">Open</span>
                      </button>
                    </li>
                    <li className="option">
                      <button onClick={() => deleteNote(note)} className="cta cta-w-icon">
                        <TrashIcon className="icon" />
                        <span className="">Delete</span>
                      </button>
                    </li>
                  </ul>
                </footer>
              </article>
            </li>
          ))}
        </ul>
      ) : (
        <div className="fallback-message">
          <p>Oops.. no notes yet</p>
        </div>
      )}
    </div>
  );
};
export default NotesList;
```

[View full code on Github](https://github.com/miracleonyenma/next-notes-app-starter/blob/starter-code/components/NotesList.js)

### The Editor component

The function of the `<Editor />`  component is simple, it gets current note data from the application state by importing `useNote()`  from `./modules/AppContext.js` and assigning it to `currentNote`. It adds a new note to the global `notes` array or updates an existing note depending on the action type.

```javascript
import { useEffect, useState } from "react";
import { CheckCircleIcon } from "@heroicons/react/solid";
import { useNote, useDispatchNote, useNotes, useDispatchNotes } from "../modules/AppContext";
import RandomID from "../modules/RandomID";
const Editor = () => {
  // the current note
  const currentNote = useNote();
  const setCurrentNote = useDispatchNote();
  // the array of saved notes
  const notes = useNotes();
  const setNotes = useDispatchNotes();
  // editor note states
  const [title, setTitle] = useState("Hola");
  const [body, setBody] = useState(
    `There once was a ship that put to sea
and the name of the ship was the billy old tea`
  );
  const [noteID, setNoteID] = useState(null);
  const [noteAction, setNoteAction] = useState("add");
  const [isSaved, setIsSaved] = useState(false);
  // function to update textarea content and height
  const updateField = (e) => {
    // get textarea
    let field = e.target;
    //set body state to textarea value
    setBody(field.value);
    // reset textarea height
    field.style.height = "inherit";
    // Get the computed styles for the textarea
    let computed = window?.getComputedStyle(field);
    // calculate the height
    let height =
      parseInt(computed.getPropertyValue("border-top-width"), 10) +
      parseInt(computed.getPropertyValue("padding-top"), 10) +
      field.scrollHeight +
      parseInt(computed.getPropertyValue("padding-bottom"), 10) +
      parseInt(computed.getPropertyValue("border-bottom-width"), 10);
    // set the new height
    field.style.height = `${height}px`;
  };
  // function to save note to saved notes array
  const saveNote = () => {
    // check if the title input & body textarea actually contain text
    if (title && body) {
      // check if note already has an ID, if it does asign the current id to the note object,
      // if not, assign a new random ID to the note object
      let id = noteID || RandomID(title.slice(0, 5), 5);
      // the note object
      let note = {
        id,
        title,
        body,
      };
      try {
        if (noteAction == "edit") {
          // edit in notes list
          setNotes({ note, type: "edit" });
          console.log({ note, noteAction, noteID, notes });
        } else {
          // add to notes list
          setNotes({ note, type: "add" });
        }
        // change isSaved state to true, thereby disabling the save button
        setIsSaved(true);
        // clear note content
        note = { title: "", body: "" };
        // clear editor
        setTitle(note.title);
        setBody(note.body);
        // clear current note state
        setCurrentNote(note);
      } catch (error) {
        console.log({ error });
      }
    }
  };
  // enable the button whenever the content of title & body changes
  useEffect(() => {
    if (title && body) setIsSaved(false);
    else setIsSaved(true);
  }, [title, body]);
  // update the editor content whenever the note context changes
  // this acts like a listener whenever the user clicks on edit note
  // since the edit note funtion, sets
  useEffect(() => {
    if (currentNote.title && currentNote.body) {
      setTitle(currentNote.title);
      setBody(currentNote.body);
      setNoteID(currentNote.id);
      setNoteAction(currentNote.action);
    }
  }, [currentNote]);
  return (
    <div className={"editor"}>
      <div className={"wrapper"}>
        <div className="editing-area">
          <div className="title">
            <input value={title} onChange={(e) => setTitle(e.target.value)} type="text" className={"form-input"} placeholder="Title" />
          </div>
          <div className="body">
            <textarea
              value={body}
              onChange={(e) => updateField(e)}
              name="note-body"
              id="note-body"
              className="form-textarea"
              cols="10"
              rows="2"
              placeholder="Write something spec ✨"
            ></textarea>
          </div>
        </div>
        <ul className={"options"}>
          <li className={"option"}>
            <button onClick={saveNote} disabled={isSaved} className="cta flex gap-2 items-end">
              <CheckCircleIcon className="h-5 w-5 text-blue-500" />
              <span className="">{isSaved ? "Saved" : "Save"}</span>
            </button>
          </li>
        </ul>
      </div>
    </div>
  );
};
export default Editor;
```

[View full code on Github](https://github.com/miracleonyenma/next-notes-app-starter/blob/starter-code/components/Editor.js)

Hopefully, we now know the basics of how the app works. Let's proceed to NextAuth, Prisma & MongoDB.



## Open Source Session Replay

_[OpenReplay](https://github.com/openreplay/openreplay) is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data._

![replayer.png](https://static.openreplay.com/images/banner-blog.png)

_Start enjoying your debugging experience - [start using OpenReplay for free](https://github.com/openreplay/openreplay)._



## Setting up NextAuth, Prisma & MongoDB

An **Adapter** in NextAuth.js connects your application to whatever database or backend system you want to use to store data for users, their accounts, sessions, etc. We'll be using the [Prisma adapter](https://next-auth.js.org/adapters/prisma). To use this Adapter, you need to install Prisma Client, Prisma CLI, and the separate `@next-auth/prisma-adapter` package:

```
yarn add next-auth @prisma/client @next-auth/prisma-adapter
yarn add prisma --dev
```

Let's set up our MongoDB cluster so we can configure our NextAuth.js to use the Prisma Adapter. Log in to MongoDB and set up a MongoDB cluster on [Atlas](https://atlas.mongodb.com/)
Go to **Projects** and create a new project:

![Create a new project in MongoDB atlas](https://blog.openreplay.com/static/f05bbe9cede9e2a71770938e0f3866a6/68382/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653304882396_image.png)

Next, we'll add permissions to our project and be assigned **Project Owner.** Next, create a database for our project.

![Create new database](https://blog.openreplay.com/static/5bb338f25044c4ce0232ef773dc36c34/68382/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653305055263_image.png)

Once we click on **Build a database**, we'll be asked to choose our database plan; you can use the free shared plan.
Next, we select the providers and region for our database cluster; you can leave it as is and use the M0 cluster, which is free.

![Creating a free M0 cluster](https://blog.openreplay.com/static/8c7defb3fc77223f01fac7095a3a23df/4b436/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653305348210_image.png)

Click on **Create Cluster**. Next, we'll have to set up our database security. Choose **username and password**, create a new user, in the **"Where would you like to connect from?"** section, choose **"My Local Environment"** and **Add My Current IP Address.** Click on **Finish**.

Next, we need to get our connection URL to connect to our cluster. Click on **Connect your application**

![Connect application](https://blog.openreplay.com/static/4e45354cdd58ef95d2a13ddf2fa04599/68382/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653305970173_image.png)

Copy the connection string provided

![Obtain the connection string for cluster](https://blog.openreplay.com/static/727a17c197f9a1bcb8a276d429077686/68382/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653306017051_image.png)

Now that we've gotten or database connection string, add it to a `.env` file. With a few other environment variables required by NextAuth.

```
// .env

DATABASE_URL=mongodb+srv://username:password@cluster0.ba9ic.mongodb.net/myFirstDatabase?retryWrites=true&w=majority
NEXTAUTH_SECRET=somesecret
NEXTAUTH_URL=http://localhost:3000
```

> 🚨 Make sure you include the database name in your MongoDB URL. For example, the database used here is `myFirstDatabase`.

### Working with Prisma

We can use the Prisma CLI tool to create a couple of Prisma-related files. Run:

```
    npx prisma init
```

This creates a basic `/prisma/schema.prisma` file. This schema is adapted for use in Prisma and based on NextAuth  main [**schema**](https://next-auth.js.org/adapters/models). We'll modify the schema to work with NextAuth; enter the following code into the schema:

```
// ./prisma/schema.prisma

datasource db {
  provider = "mongodb"
  url      = env("DATABASE_URL")
}
generator client {
  provider        = "prisma-client-js"
}
model Account {
  id                String  @id @default(auto()) @map("_id") @db.ObjectId
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String? @db.String
  access_token      String? @db.String
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String? @db.String
  session_state     String?
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  @@unique([provider, providerAccountId])
}
model Session {
  id           String   @id @default(auto()) @map("_id") @db.ObjectId
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
  id            String    @id @default(auto()) @map("_id") @db.ObjectId
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
}
model VerificationToken {
  identifier String   @id @default(auto()) @map("_id") @db.ObjectId
  token      String   @unique
  expires    DateTime
  @@unique([identifier, token])
}
```

Now that we have written our schema, we can create the collections in our database. Using the Prisma using the CLI tool, run `npx prisma db push`, and you should see:

![Sync database to MongoDB atlas](https://blog.openreplay.com/static/47947e9e4e8eb63cc53b541bdfee4a64/94352/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653317717076_image.png)

We should see our new collections if we check our database collection in Atlas.

![MOngoDB atlas new database with collections](https://blog.openreplay.com/static/4cd83d0e8aa880e5096f0a07e94717cf/68382/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653318883658_image.png)

Awesome! Now, to interact with our database, we must first generate the Prisma Client. Use the Prisma CLI to generate the Prisma Client:

```
npx prisma generate
```

This command reads our Prisma schema and generates a version of Prisma Client that is *tailored* to our models.

![Generate prisma client](https://blog.openreplay.com/images/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653319186477_image.png)

We can start using PrismaClient to interact with our database. We'll use a single `PrismaClient` instance that we can import into any file where needed. Create a new `./prisma/prisma.js` file:

```javascript
// prisma/prisma.js

import { PrismaClient } from '@prisma/client'
let prisma
if (process.env.NODE_ENV === 'production') {
  prisma = new PrismaClient()
} else {
  if (!global.prisma) {
    global.prisma = new PrismaClient()
  }
  prisma = global.prisma
}
export default prisma
```

Now, let's proceed to complete our NextAuth integration.

### Set up our Google application

We'll be using Google provider for this project, and so we need to get our environment variables to configure our Google provider. First, we log into [Google Cloud console](https://console.cloud.google.com/home/dashboard),

![Google cloud console](https://blog.openreplay.com/static/d57f9f6873da5284b8d4fa3f5d332222/68382/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653296825594_image.png)

Click on the dropdown which opens a modal; in the top right corner of the modal, click on **New Project**, to create a new project.

![Create new google cloud project](https://blog.openreplay.com/static/9de5077be2a59bf1c3f438245ef54c52/68382/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653296860445_image.png)

Enter the project name and click **create**. Once the project is created, click on **SELECT PROJECT**in the notifications modal.

![Selct new project](https://blog.openreplay.com/static/78f2b0b89368baaa8164399f7cf7b748/68382/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653296903692_image.png)

Now, in our new app we can open the side bar, go to [APIs & Services](https://console.cloud.google.com/apis?project=next-notes-app) > [Credentials](https://console.cloud.google.com/apis/credentials?project=next-notes-app). The first thing we need to do is to configure the consent screen:

![Configure consent screen](https://blog.openreplay.com/static/c2c4d2d010da9373026b0e1ff2195633/68382/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653297559919_image.png)

Then choose **External** User type.

![Chhose external user type](https://blog.openreplay.com/static/202c8eb490a13446bda10586a7e07d1b/68382/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653297679270_image.png)

On the next screen, we add our app information.

![Add app information](https://blog.openreplay.com/static/700cdddae7b4696f252e89dddd167178/68382/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653297862569_image.png)

Then, we add the developer information and click on **Save & Continue** on this screen and the following screens until it's complete, and we go back to the dashboard. In the side menu, under **API & CREDENTIALS,** click on **Credentials** to create a new **OAuth Client ID.

![Create OAuth client ID credential](https://blog.openreplay.com/static/b311b5b7258313c6288e965d6849ef22/68382/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653298160958_image.png)

In the next screen, we'll select the application type and name,

![Select application type](https://blog.openreplay.com/static/c42c12d0115e2925f71ce53259a43923/68382/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653298510985_image.png)

and add an authorized [Google URI for development](https://next-auth.js.org/providers/google#configuration) - `http://localhost:3000/api/auth/callback/google`

![Add authorized URI](./images/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653298582743_image.png)

Click on **Create**, and after the client ID has been created, we'll have our Client ID and Client Secret. Create a `.env` file to save these variables

```
// .env

GOOGLE_CLIENT_ID=<client_ID_here>
GOOGLE_CLIENT_SECRET=<client_secret_here>
DATABASE_URL=mongodb+srv://username:password@cluster0.ba9ic.mongodb.net/myFirstDatabase?retryWrites=true&w=majority
NEXTAUTH_SECRET=somesecret
NEXTAUTH_URL=http://localhost:3000
```

### Set up NextAuth with the Prisma adapter

To add NextAuth.js to a project, create a file called `[...nextauth].js` in `pages/api/auth`. This contains the dynamic route handler for NextAuth.js, which will also contain all your global NextAuth.js configurations. All requests to `/api/auth/*` (`signIn`, `callback`, `signOut`, etc.) will automatically be handled by NextAuth.js. We also configure your NextAuth.js to use the Prisma Adapter and Prisma client.

```javascript
// ./pages/api/auth/[...nextauth].js

import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import prisma from "../../../prisma/prisma";

export default NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
  ],
  callbacks: {
    async session({ session, token, user }) {
      session.user.id = user.id;
      return session;
    },
  },
});
})
```

Take note of the `callbacks` object; this is where we populate the user session with their `id` as NextAuth does not provide this value by default.

### Configure the NextAuth Shared session state

The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
To be able to use `useSession` first you'll need to expose the session context, `[**<SessionProvider />**](https://next-auth.js.org/getting-started/client#sessionprovider)`, at the top level of your application:

```javascript
// ./pages/_app.js

import { SessionProvider } from "next-auth/react";
import { NoteProvider } from "../modules/AppContext";
import DefaultLayout from "../layouts/default";

function MyApp({ Component, pageProps: { session, ...pageProps } }) {
  return (
    <SessionProvider session={session}>
      <NoteProvider>
        <DefaultLayout>
          <Component {...pageProps} />
        </DefaultLayout>
      </NoteProvider>
    </SessionProvider>
  );
}
export default MyApp;
```

Instances of `useSession` will then have access to the session data and status. The `<SessionProvider />` also keeps the session updated and synced between browser tabs and windows.

### Add Login functionality

To add the login and logout actions, we'll create a component `./components/AuthBtn.js`, which will be placed in our `./components/SiteHeader.js` component.

```javascript
// ./components/AuthBtn.js

import { ChevronDownIcon, RefreshIcon } from "@heroicons/react/solid";
import { useSession, signIn, signOut } from "next-auth/react";
import Image from "next/image";
const AuthBtn = () => {
  const { data: session, status } = useSession();
  if (status === "loading") {
    return (
      <div className="auth-btn">
        <div className="auth-info">
          <RefreshIcon className="icon animate-spin" />
        </div>
      </div>
    );
  }
  if (status === "unauthenticated") {
    return (
      <div className="auth-btn">
        <button onClick={() => signIn()}>Login</button>
      </div>
    );
  }
  return (
    <div className="auth-btn">
      <div className="auth-info pr-2">
        <Image src={session.user.image} alt={session.user.name} width={30} height={30} className="rounded-full" />
        <p>Hi, {session.user.name}</p>
      </div>
      <div className="dropdown">
        <button className="dropdown-btn !py-1">
          <ChevronDownIcon className="icon" />
        </button>
        <ul className="dropdown-list opacity-0 invisible">
          <li className="dropdown-item">
            <button onClick={() => signOut()} className="cta">
              Logout
            </button>
          </li>
        </ul>
      </div>
    </div>
  );
};
export default AuthBtn;
```

Here, we render our UI based on the state returned by `status`, which maps to three possible session states:

- `"loading"` - A rotating spinning Icon
- `"authenticated"` - Username, picture and logout dropdown
- `"unauthenticated"` - A login button

**Quick note:** To display images from another domain using Next.js `<Image />`,  we need to [add it to a list of domains](https://nextjs.org/docs/messages/next-image-unconfigured-host) in our `./next.config.js` file

```javascript
// next.config.js

const nextConfig = {
  reactStrictMode: true,
  images: {
    domains: ['lh3.googleusercontent.com'],
  },
}
module.exports = nextConfig
```

Let's see it in action. Run `yarn dev` to get:

![Basic authentication example](https://blog.openreplay.com/30749c953db3264affd5d9861efc16f4/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653330702745_736c324f-c7bb-4f3d-b23f-556a8c44d569_gif.gif)

You can [view the deployed app hosted on Netlify](https://basic-auth-next-notes-app.netlify.app/) and the current code on the [](https://github.com/miracleonyenma/next-notes-app-starter/tree/basic-crud)`[basic-auth](https://github.com/miracleonyenma/next-notes-app-starter/tree/basic-crud)` [branch of the  repository on GitHub](https://github.com/miracleonyenma/next-notes-app-starter/tree/basic-auth)

Next, we update our Prisma schema with the Note model so that we can manage notes in our MongoDB database.

## Add Note Model to Prisma Schema

In our `./prisma/schema.prisma` file, we're going to add `Note` model:

```
// ./prisma/schema.prisma

model Note {
  id    String @id @default(auto()) @map("_id") @db.ObjectId
  title String
  body  String
  userId String?
  user   User?   @relation(fields: [userId], references: [id], onDelete: Cascade)
  @@unique([id, userId])
}
```

We'll also have to add the `Note` reference to our `User` model:

```
// ./prisma/schema.prisma

model User {
  id            String    @id @default(auto()) @map("_id") @db.ObjectId
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
  notes          Note[] //add note reference
}
```

Once again, to sync with our database, run `npx prisma db push`

![Sync database with updated Prisma scheam](https://blog.openreplay.com/static/ab4db4d5fd3664a68aa0394b1e09a06c/94352/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653367034765_image.png)

We should see the new `Note` collection if we check our MongoDB database.

![NOte collection in MongoDB](https://blog.openreplay.com/static/0d456a6ecba558c6393281aebc539421/68382/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653367156687_image.png)

## Add create new note functionality

Let's create our CRUD functions. First, a `createNote` function will add a note to our database with Prisma. As we progress, we'll create reading, updating, and deleting functions. Create a new file `./prisma/note.js` which will contain all our CRUD functions:

```javascript
// ./prisma/Note.js
import prisma from "./prisma";

// READ
//get unique note by id
export const getNoteByID = async (id) => {
  const note = await prisma.note.findUnique({
    where: {
      id,
    },
    include: {
      user: true,
    },
  });
  return note;
};

// CREATE
export const createNote = async (title, body, session) => {
  const newNote = await prisma.note.create({
    data: {
      title,
      body,
      user: { connect: { email: session?.user?.email } },
    },
  });
  const note = await getNoteByID(newNote.id);

  return note;
};
```

Here,  our create function accepts a few parameters - `title`, `body` and `session`. `session` will contain the current session data, including information about the user, specifically `user.email`. We're using `prisma.note.create()` to create a new note by passing an object with the `data` key, which is an object containing `title`, `body`, and `user`. Since the `user` field is relational, we're using `connect` to connect the new note to an existing user with the `email` provided. Next, we must create an API endpoint to run this function.

### Add Create Note API endpoint

Create a new file `./pages/api/note.js`.

```javascript
// pages/api/note.js

import { createNote } from "../../prisma/Note";
import { getSession } from "next-auth/react";
export default async function handle(req, res) {
  // Get the current session data with {user, email, id}
  const session = await getSession({ req });
  // Run if the request was a post request
  if (req.method == "POST") {
    // Get note title & body from the request body
    const { title, body } = req.body;
    // Create a new note
    // also pass the session which would be use to get the user information
    const note = await createNote(title, body, session);
    // return created note
    return res.json(note);
  }
}
```

Here, we're creating an API endpoint. Whenever a request goes to `/api/note`, we handle it here. First, we check we get the authenticated user session with the `getSession` helper function from NextAuth.js. For a `POST` request, we get the `title` and `body` from the request. We then run the `createNote` function that we created earlier. Next, let's send a create request from our frontend in the `<Editor />` component.

### Send create request from Frontend

🚨 To ensure that only authenticated users can create notes, in the editor component, we must render only when the  `session` status is "authenticated."

```javascript
// ./components/Editor.js

const Editor = () => {
  // ..
  return (
    status === "authenticated" && (
        <div className={"editor"}> {/* ... */} </div>
    )
  );

};
```

In our `./components/Editor.js` file, using the `fetch` API, we'll send a `POST` request to our create endpoint `/api/note` and save the data to our `Notes` context state. Let's edit our `saveNote` function.

```javascript
// ./components/Editor.js

// ...
  const saveNote = async () => {
    if (title && body) {
      // ...
      try {
        if (noteAction == "edit") {
          // ...
        } else {
          // send create request with note data
          let res = await fetch("/api/note", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(note),
          });

          const newNote = await res.json();
          console.log("Create successful", { newNote });
          // add to notes list (global context state)
          setNotes({ note: newNote, type: "add" });
        }
        // ...
      }
    }
  }
// ...
```

Let’s see that in action:

![Create note functionality](https://blog.openreplay.com/41842d2bcc97de40fa24ae0181eb0d3a/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653398460909_db368db9-a7dc-441e-95f4-491a3210bda3_gif.gif)

Next, let's see how we can update our notes.

## Add update note functionality

Similarly, we’ll create an update function. In our `./prisma/note.js` file, create a new `updateNote()` function.

```javascript
// ./prisma/Note.js
// ...

// UPDATE
export const updateNote = async (id, updatedData, session) => {
  let userId = session?.user.id;
  const updatedNote = await prisma.note.update({
    where: {
      id_userId: {
        id,
        userId,
      },
    },
    data: {
      ...updatedData,
    },
  });
  const note = await getNoteByID(updatedNote.id);
  return note;
};
```

Here, we get the note `id`, `updatedData`, and user `session`. From the user `session`, we get the `userId`.
In `prisma.note.update`, we filter the note to update by the `userId` and note `id` by combining the unique fields and concatenating them with an underscore.

```javascript
where: {
    id_userId: {
      id,
      userId,
    },
  },
},
```

And then pass the `updatedData` to `data`. Next, we'll create the API endpoint to call our update function.

### Add update note API endpoint

Back in our `./pages/api/note.js` file,  for `PUT` requests, we’ll get and pass the note `id`, `title` , `body`  and `session` and pass it into our `updateNote()` function.

```javascript
// ./pages/api/note.js
// ...
export default async function handle(req, res) {
  // Run if the request is a POST request
  // ...

  // Run if the request is a PUT request
  else if (req.method == "PUT") {
    const { id, title, body } = req.body;

    // const updatedData = {title, body}
    // Update current note
    // also pass the session which would be use to get the user information
    const note = await updateNote(id, { title, body }, session);
    // return updated note
    return res.json(note);
  }
}
```

Now, back in our `./components/Editor.js` component, let's add the request for a note update.

```javascript
// ./components/Editor.js

// ...
  const saveNote = async () => {
    if (title && body) {
      // ...
      try {
        if (noteAction == "edit") {
          // add note id to note data
          note.id = noteID;
          // send request to edit note
          let res = await fetch("/api/note", {
            method: "PUT",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(note),
          });
          // update note
          const updatedNote = await res.json();
          console.log("Update successful", { updatedNote });
          // edit in notes list
          setNotes({ note: updatedNote, type: "edit" });
        } else {
          // send create request with note data
          // ...
        }
        // ...
      }
    }
  }
// ...
```

Here, we get the note `id` from the `noteID` state variable. In our `fetch`  function, we send a `PUT` request with the `note` data and save the response to our `Notes` context state. Let’s see it in action:

![Update note functionality](https://blog.openreplay.com/2236304fc47a1e8e6f507c7425fbd5fb/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653403607535_094125bb-5ff8-41d2-bc52-b8844d8f21b6_gif.gif)

So far, we've implemented Create and Update functionality to our application, but if we still refresh, we see that our notes disappear since we can't yet get notes from the database. Next, we'll have to make some changes to be able to load notes from our database.

## Update view to display notes from the database

First, we'll create more helper functions to get all notes from the authenticated user.

```javascript
  // ./prisma/Note.js
  // ...

  // get notes by user
  export const getAllNotesByUserID = async (id) => {
    const notes = await prisma.note.findMany({
      where: {
        userId: id,
      },
      include: {
        user: true,
      },
    });
    return notes;
  };
```

Next, on our home page `./pages/index.js`, we'll get the notes on the server-side using `getServerSideProps`

```javascript
// ./pages/index.js

import { getSession } from "next-auth/react";
const getAllNotesByUserID = require("../prisma/Note").getAllNotesByUserID;

// ...

export const getServerSideProps = async ({ req, res }) => {
  const session = await getSession({ req });
  if (!session) {
    res.statusCode = 403;
    return { props: { notes: [] } };
  }
  const notes = await getAllNotesByUserID(session?.user?.id);
  return {
    props: { notes },
  };
};

const Home = ({ notes }) => {
  const [showEditor, setShowEditor] = useState(true);
  return (
    <>
      {/* ... */}
      <div className={HomeStyles.container}>
        <main className={HomeStyles.main}>
          <div className="wrapper m-auto max-w-8xl">
            {/* ... */}

            {/* Note list component */}
            <NotesList retrieved_notes={notes} />
          </div>
        </main>
      </div>
    </>
  );
};
```

🚨 Here, we're using `require` to import our `getAllNotesByUserID` helper function to avoid Next.js trying to run it on the client-side. Then, in the `./components/NoteList.js` component, we get `retrieved_notes` as props, and in a `useEffect` hook, replace the application notes state with the fetched notes:

```javascript
// ./components/NoteList.js

import { useEffect } from "react";
import Image from "next/image";
// ...

const NotesList = ({ retrieved_notes, showEditor }) => {
  // ...
  useEffect(() => {
    // replace notes in notes context state
    setNotes({ note: retrieved_notes, type: "replace" });
  }, [retrieved_notes]);

  return (
    <div className="notes">
      {notes.length > 0 ? (
        <ul className="note-list">
          {notes.map((note) => (
            <li key={note.id} className="note-item">
                {/* ... */}
                <footer className="note-footer">
                  <ul className="options">
                    <li className="option">
                      {/* add user image to note footer */}
                      <Image src={note.user.image} alt={note.user.name} width={32} height={32} className="rounded-full" />
                    </li>
                    {/* ... */}
                  </ul>
                </footer>
              </article>
            </li>
          ))}
        </ul>
      ) : (
        {/* ... */}
      )}
    </div>
  );
};
```

Also, in our `.modules/AppContext.js` file, we add support for the `replace` action type in our reducer function.

```javascript
const notesReducer = (state, action) => {
  // get the note object and the type of action by destructuring
  const { note, type } = action;

  // if "replace"
  // replace the entire array with new value
  if (type === "replace") return note;

  // ...
}
```

See in action:

![Add read notes functionality](https://blog.openreplay.com/e9fa4b640c79e45c827e532640f65fa5/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653481455553_e11e6f6d-4785-45a7-acb3-188777cfaa74_gif.gif)

Awesome.We can now get our notes from the database and update the application state when the page loads.

## Add delete note functionality

To delete a note, we need to create a delete helper function. In our `./prisma/Note.js` file, add this function:

```javascript
// ./prisma/Note.js
// ...

// DELETE
export const deleteNote = async (id, session) => {
  let userId = session?.user.id;
  const deletedNote = await prisma.note.delete({
    where: {
      id_userId: {
        id,
        userId,
      },
    },
  });
  return deletedNote;
};
```

Next, create a delete handler in our `./pages/api/note.js` file.

```javascript
// pages/api/note.js

export default async function handle(req, res) {
  if (req.method == "POST") {
    // ... Create a new note
  }
  else if (req.method == "PUT") {
    // ... Update current note
  }
  // Run if the request is a DELETE request
  else if (req.method == "DELETE") {
    const { id } = req.body;
    const note = await deleteNote(id, session);
    // return deleted note
    return res.json(note);
  }
}
```

And finally, in our `./components/NoteList.js` component, we'll send a `DELETE` request in our `deleteNote` function.

```javascript
// ./components/NoteList.js

const NotesList = ({ retrieved_notes, showEditor }) => {
  // ...
  // function to delete note by using the setNotes Dispatch notes function
  const deleteNote = async (note) => {
    let confirmDelete = confirm("Do you really want to delete this note?");
    try {
      let res = await fetch(`/api/note`, {
        method: "DELETE",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(note),
      });
      const deletedNote = await res.json();
      confirmDelete ? setNotes({ note: deletedNote, type: "remove" }) : null;
    } catch (error) {
      console.log(error);
    }
  };

// ...

}
```

Let’s see it in action:

![Add Delete functionality](https://blog.openreplay.com/images/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653482543801_6966f1c4-f29b-4e4a-b68d-a029c386d2cb_gif.gif)

Awesome! Finally, we can create dynamic routes to display individual notes. Create a new dynamic page `./pages/note/[id].js`

```javascript
// ./pages/note/[id].js

import Head from "next/head";
import Image from "next/image";
import { getSession } from "next-auth/react";
const getNoteByID = require("../../prisma/Note").getNoteByID;
import HomeStyles from "../../styles/Home.module.css";
export const getServerSideProps = async ({ req, res, params }) => {
  const session = await getSession({ req });
  console.log({ params });
  const { id } = params;
  if (!session) {
    res.statusCode = 403;
    return { props: { note: null } };
  }
  const note = await getNoteByID(id);

  return {
    props: { note },
  };
};
const Note = ({ note }) => {
  if (note == null) {
    return (
      <>
        <Head>
          <title>Login to view note</title>
          <meta name="description" content="Login to view this note" />
          <link rel="icon" href="/favicon.ico" />
        </Head>
        <div className={HomeStyles.container}>
          <main className={HomeStyles.main}>
            <header className="max-w-4xl mt-24 mx-auto">
              <h1 className="text-4xl">Oops... You have to login to view this note</h1>
            </header>
          </main>
        </div>
      </>
    );
  }
  return (
    <>
      <Head>
        <title>{note.title}</title>
        <meta name="description" content={`By ${note.user.name}`} />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div className={HomeStyles.container}>
        <main className={HomeStyles.main}>
          <article className="note max-w-4xl m-auto mt-20">
            <header className="note-header">
              <h2 className="text-4xl">{note.title}</h2>
            </header>
            <main className=" px-4">
              <p className="text-xl">{note.body}</p>
            </main>
            <footer className="note-footer">
              <ul className="options px-4">
                <li className="option">
                  {/* add user image to note footer */}
                  <Image src={note.user.image} alt={note.user.name} width={48} height={48} className="rounded-full" />
                </li>
              </ul>
            </footer>
          </article>
        </main>
      </div>
    </>
  );
};
export default Note;
```

Next, in the `./components/NoteList.js` component, we'll use Next.js `<Link>` component to wrap our "open" button to route each note page by its `id.`

```javascript
// ./components/NoteList.js

//...
<li className="option">
  <Link href={`/note/${note.id}`} target={`_blank`} rel={`noopener`}>
    <button className="cta cta-w-icon">
      <ExternalLinkIcon className="icon" />
      <span className="">Open</span>
    </button>
  </Link>
</li>

another word
//...
```

Now, if we click the open button, we'll be taken to the note page.

![Dynamic note page](https://blog.openreplay.com/5e33fe7cc2a2c17ea41ab3be2dad3eeb/s_CE27442C5AC72289167E943C17ECE23EB161F3FF28BBC9944BCA83CB33294E6D_1653488962577_a2bd2af5-a2aa-4f31-ac22-92c2a90ee147_gif.gif)

We're done! Awesome! You can also see the [outcome hosted on Netlify](https://basic-crud-next-notes-app.netlify.app/) and the code on the `[basic-crud](https://github.com/miracleonyenma/next-notes-app-starter/tree/basic-crud)` [branch of the project repository on GitHub](https://github.com/miracleonyenma/next-notes-app-starter/tree/basic-crud)

## Conclusion

It's a popular fact that Next.js and Prisma are great for building fast full-stack applications. In this tutorial, we built a notes application where users can log in to the application thanks to NextAuth, create, view, edit and delete notes. All user information and notes are saved to a MongoDB database. To interact with the database from the Next.js application, we used the Prisma ORM, which helps us define our schema and create and connect to our database for us to manage it.

## Further reading & resources

Here are some excellent links and resources to help you as you explore Next.js & Prisma

### Code & hosted examples for this project

- [Starter template on GitHub](https://github.com/miracleonyenma/next-notes-app-starter/tree/starter-code)
- [Starter example hosted on Netlify](https://starter-next-notes-app.netlify.app/)
- [Basic auth implementation code on the `basic-auth` branch of the project GitHub repo](https://github.com/miracleonyenma/next-notes-app-starter/tree/basic-auth)
- [Basic auth example hosted on Netlify](https://basic-auth-next-notes-app.netlify.app/)
- [Final project with basic CRUD functionality on the `basic-crud` branch of the project repo](https://github.com/miracleonyenma/next-notes-app-starter/tree/basic-crud)
- [Final example hosted on Netlify](https://basic-crud-next-notes-app.netlify.app/)

### Documentation & Articles

- [How to Build a Fullstack App with Next.js, Prisma, and PostgreSQL](https://vercel.com/guides/nextjs-prisma-postgres)
- [Using Prisma ORM with MongoDB in Next.js](https://itnext.io/using-prisma-orm-with-mongodb-in-next-js-e42b1f7543e6)
- [Static Sites with Next.js 9.3 and Prisma](https://leerob.io/blog/next-prisma)
- [Next.js Docs](https://nextjs.org/docs/getting-started)
- [Getting Started with Next Auth](https://next-auth.js.org/getting-started/introduction)
- [NextAuth + Prisma Adapter](https://next-auth.js.org/adapters/prisma)

[![newsletter](../newsletter.png)](https://newsletter.openreplay.com/)