Explore Full Stack Magic:Next.js + Prisma + Next-auth + vercel/postgres

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.

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

Initialization Project

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

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/, 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.

  1. 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.
  2. 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:

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:

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

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

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.

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

Then change the user-related model

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:

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

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

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

import { NextAuthNextAuthOptions } 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.

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
import { useSession } from 'next-auth/react';

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

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

  // ...
}
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: {},
  };
}
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
"scripts": {
 "build": "prisma generate && next build"
}
  1. 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.

  1. 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

  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.

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;
};
  1. 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
  2. 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. 👻