Explore Full Stack Magic：Next.js + Prisma + Next-auth + vercel/postgres
Tags: webdev, javascript, react, opensource
Published: 2023-11-28 15:16:03

Recently, I completed a new website project to collecting Github Profiles and Readme Components. Initially, I didn't plan to develop it into a full-stack application, and features such as liking and collect were based on local storage. However, with the gradual iterations after the website's release, I felt that adding full-stack support might be a necessary direction. Not only does it contribute to future project expansion, but it also enhances the overall user experience. So, in my spare time over the course of about a week, I transformed the project into a fully-fledged full-stack website. Here is the link to my project.

+ Link: [https://bubble-awesome-profile.vercel.app](https://bubble-awesome-profile.vercel.app)

+ Github: [https://github.com/LHRUN/bubble](https://github.com/LHRUN/bubble)


![site preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i5j8ctgh4whxffkxnmog.png)

After completing the full-stack version of the website, I thought about documenting the process in an article. All the code for this article can be found in the project. In the upcoming posts, I will gradually share content about topics such as authentication, the use of Prisma and PostgreSQL, and ultimately guide you through the process of building a full-stack website.

## Tech Stack

+ full-stack framework [Next.js](https://nextjs.org/)
+ database tool [Prisma](https://www.prisma.io/)
+ database [PostgreSQL](https://www.postgresql.org/)
+ authenticate [NextAuth](https://next-auth.js.org/)
+ deployment platform [Vercel](https://vercel.com/)

## Initialization Project

First, run the `npx create-next-app@latest` command in the terminal, and then follow the prompts for configuration.

```bash
What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias (@/*)? No / Yes
What import alias would you like configured? @/*
```

After a successful run, navigate to the project directory and execute `npm run dev`.

After successful startup, you can start building the base environment

1. First, upload the project to GitHub by creating a repository and linking the remote address. Then, push the code.
2. Go to the Vercel [https://vercel.com/](https://vercel.com/), log in, and choose GitHub. When uploading the project, keep the default settings in "Configure Project" and click "Deploy" directly. Environment variables can be adjusted when needed. Wait automatic deployment to complete.


![vercel create project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1wsw20i1tajdv8993z1p.png)

3. After successful deployment, create the database. Select PostgreSQL in the Storage tab, enter a name and select a region and create it. Once created the database will be linked to your project.
4. Local code pulling environment variables:
  - Run `npm i -g vercel@latest` to install the Verlcel CLI.
  - Run `verlcel link` to link your vercel project.
  - Run `vercel env pull .env` to pull the latest environment variables.

## Connecting to the database

Run `npm install prisma --save-dev` to install the Prisma CLI and create a `Prisma` folder, add the `schema.prisma` file to it, and then add a few models as follows:

```prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider  = "postgresql"
  url       = env("POSTGRES_PRISMA_URL") // uses connection pooling
  directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection
}

model Post {
  id        String     @default(cuid()) @id
  title     String
  content   String?
  published Boolean @default(false)
  author    User?   @relation(fields: [authorId], references: [id])
  authorId  String?
}

model User {
  id            String       @default(cuid()) @id
  name          String?
  email         String?   @unique
  createdAt     DateTime  @default(now()) @map(name: "created_at")
  updatedAt     DateTime  @updatedAt @map(name: "updated_at")
  posts         Post[]
  @@map(name: "users")
}
```

Prisma is to define the table structure through the model, for example, the above is to build a Post table and a User table, and then these two tables through the posts in the User and the author in the Post to establish a one-to-many table relationship.

Then run the `npx prisma db push` command to synchronize the defined data model to the remote database. You should then see the following prompt. This means that the table has been created successfully

```
🚀  Your database is now in sync with your schema.
```

Then run `npx prisma studio` in `localhost:5555` to see your table structure, which is of course now all empty data.

## Getting database data into a views

To get the database data into the view, you first need to install `@prisma/client`, run the command `npm install @prisma/client`.

Then run `npx prisma generate`, and note that you will need to run this command every time you change the Prisma Schema file thereafter. It will generate the TypeScript or JavaScript code corresponding to the table structure, the functions used to perform database queries, inserts, updates, and deletes, as well as the associated type definitions.

Next, create a prisma.ts file in your project, which can usually be placed in the lib folder. Add the following code to that file:

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

let prisma: PrismaClient;

if (process.env.NODE_ENV === 'production') {
  prisma = new PrismaClient();
} else {
  if (!global.prisma) {
    global.prisma = new PrismaClient();
  }
  prisma = global.prisma;
}

export default prisma;
```

This code is used to configure and export a Prisma instance. In a production environment, a new PrismaClient instance is created for each request, while in a development environment, it reuses the global instance to prevent running out of data connections. This setup improves performance and reduces resource usage.

Then PrismaClient has four basic APIs for create, delete, update, and read, For details, see [CRUD](https://www.prisma.io/docs/concepts/components/prisma-client/crud)

```ts
const user = await prisma.user.create({
  data: {
    email: 'elsa@prisma.io',
    name: 'Elsa Prisma',
  },
})

const user = await prisma.user.findUnique({
  where: {
    email: 'elsa@prisma.io',
  },
})

const updateUser = await prisma.user.update({
  where: {
    email: 'elsa@prisma.io',
  },
  data: {
    name: 'Elsa',
  },
})

const deleteUser = await prisma.user.delete({
  where: {
    email: 'elsa@prisma.io',
  },
})
```

For example, if you want to find a post

```ts
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
  const post = await prisma.post.findUnique({
    where: {
      id: String(params?.id),
    },
    include: {
      author: {
        select: { name: true },
      },
    },
  });
  return {
    props: post,
  };
};
```

## NextAuth Authentication

First install the two dependencies `next-auth`, `@next-auth/prisma-adapter`.

```bash
npm install next-auth @next-auth/prisma-adapter
```

Then change the user-related model

```prisma
model User {
  id              String         @id @default(uuid())
  name            String
  email           String?        @unique
  emailVerified   DateTime?      @map("email_verified")
  image           String?
  createdAt       DateTime       @default(now())
  updatedAt       DateTime       @updatedAt
  posts         Post[]

  @@map("users")
}

model Account {
  id                String   @id @default(cuid())
  userId            String   @map("user_id")
  type              String?
  provider          String
  providerAccountId String   @map("provider_account_id")
  token_type        String?
  refresh_token     String?  @db.Text
  access_token      String?  @db.Text
  expires_at        Int?
  scope             String?
  id_token          String?  @db.Text
  createdAt         DateTime @default(now())
  updatedAt         DateTime @updatedAt
  user              User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
  @@map("accounts")
}

model Session {
  id           String   @id @default(cuid())
  userId       String?  @map("user_id")
  sessionToken String   @unique @map("session_token") @db.Text
  accessToken  String?  @map("access_token") @db.Text
  expires      DateTime
  user         User?    @relation(fields: [userId], references: [id], onDelete: Cascade)
  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt

  @@map("sessions")
}

model VerificationRequest {
  id         String   @id @default(cuid())
  identifier String
  token      String   @unique
  expires    DateTime
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt

  @@unique([identifier, token])
}
```

Then, using GitHub as an example, you need to create an OAuth App. follow these steps:

1. Login to your GitHub account and click Settings.
2. At the bottom of the Settings page, find Developer Settings and switch to OAuth Apps.
3. Click "Register a new application" and fill in the name and domain name. In a local development environment, the Homepage URL can be `http://localhost:3000` and the Authorization callback URL can be `http://localhost:3000/api/auth/callback/github`.

After successful creation, copy the generated Client ID and Client Secret and add them to the `.env` file in your project:

```dotenv
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
SECRET= // Required for production environments, content can be customized
```

This way, your project will be able to do GitHub OAuth authentication with these keys. Also make sure that the `.env` file is not exposed, you can add it now to the `.gitignore`

Then you need to add the following code to your root component

```tsx
import { SessionProvider } from 'next-auth/react';

const Home = () => {
  return (
    <SessionProvider>
      ...
    </SessionProvider>
  );
};

export default Home;
```

Then, create a specific route `api/auth/[... .nextauth].ts` for NextAuth to use and add the following code

```ts
// api/auth/[...nextauth].ts

import { NextAuth， NextAuthOptions } from 'next-auth';
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import GitHubProvider from 'next-auth/providers/github';
import prisma from '@/lib/prisma';

const authOptions: NextAuthOptions = {
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_CLIENT_ID as string,
      clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
      httpOptions: {
        timeout: 10000, // wait for response time, because the local environment often login timeout, so change this configuration
      }
    })
  ],
  adapter: PrismaAdapter(prisma),
  secret: process.env.SECRET, // required for production environments
  callbacks: {
    // triggered by getSession and useSession calls
    // documents https://next-auth.js.org/configuration/callbacks
    async session({ session, user }) {
      if (user.id && session?.user) {
        session.user.userId = user.id;
      }
      return session;
    }
  }
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };
```

Then add the following code where you want to click login and it will normally take you to the github authorization page.

```ts
import { signIn } from 'next-auth/react';

const clickSignIn = () => {
  signIn('github')
}
```

Getting user session data can be done via the following APIs

+ Client：`useSession`, `getSession`
+ Server: `getServerSession`

```ts
import { useSession } from 'next-auth/react';

function MyComponent() {
  const { data: session } = useSession();

  if (session) {
    console.log('Logged in as:', session.user);
  }

  // ...
}
```

```ts
import { getSession } from 'next-auth/react';

export async function getServerSideProps(context) {
  const session = await getSession(context);

  if (session) {
    console.log('Logged in as:', session.user);
  }

  return {
    props: {},
  };
}
```

```ts
import { getServerSession } from 'next-auth/react';

export async function POST(req: NextRequest) {
  const session = await getServerSession();
  const userId = session?.user?.userId;

  // validation session
  if (!userId) {
    return responseFail(HTTP_CODE.NOT_LOGGED);
  }

  //...
}
```


## Vercel Deployment Notes

1. The build command needs to be preceded by `prisma generate`, so the build command in package.json can be changed to

```json
"scripts": {
 "build": "prisma generate && next build"
}
```

2. Since only one callback path is supported in the GitHub OAuth App, you need to create multiple OAuth Apps for different environments. set up environment variables on Vercel, and add the specific keys for each OAuth App to the environment variables. vercel supports multiple environment variable setups.


![env var](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4603scfumooz8mbvc9yw.png)


3. Make sure to add the `.env` file to `.gitignore` and don't expose it!

## Problems In Dev

1. It is very easy to fail to login in, after many times of checking still can't find the reason, for the time being, it will be categorized as a network error, if you fail to login in you can try to switch the wifi or switch your network, of course, this is mainly in the local development environment of the problem, the production environment is still very smooth. Related [issue](https://github.com/nextauthjs/next-auth/issues/3920)

2. In Next.js 14, passing the `request` object to `getServerSession` will result in an error, so try writing it like I did, and then introducing the function wherever you need to use it.

```ts
import { getServerSession as originalGetServerSession } from 'next-auth/next';

export const getServerSession = async () => {
  const req = {
    headers: Object.fromEntries(headers() as Headers),
    cookies: Object.fromEntries(
      cookies()
        .getAll()
        .map((c) => [c.name, c.value])
    )
  } as any;
  const res = { getHeader() {}, setCookie() {}, setHeader() {} } as any;

  const session = await originalGetServerSession(req, res, authOptions);
  return session;
};
```

3. When using authenticated login platforms such as GitHub, Google, etc., if the email is unique, it will cause the login to fail. This is due to the fact that the email is defined as a unique value in the model
4. When I initially wrote the user-related model I kept getting type errors when following the example on the official website, so if that's the case for you too, try the set of base models I wrote above

## Conclusion

This article just roughly summarizes the process of building a full-stack website, in which there are some details not written, but you follow this process to build your website is no problem. Welcome to discuss and exchange questions. Thanks. 👻