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
- full-stack framework Next.js
- database tool Prisma
- database PostgreSQL
- authenticate NextAuth
- deployment platform Vercel
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
- First, upload the project to GitHub by creating a repository and linking the remote address. Then, push the code.
- 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.
- 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.
- 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:
- Login to your GitHub account and click Settings.
- At the bottom of the Settings page, find Developer Settings and switch to OAuth Apps.
- 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 behttp://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 { 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.
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
- 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"
}
- 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.
- Make sure to add the
.env
file to.gitignore
and don’t expose it!
Problems In Dev
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
In Next.js 14, passing the
request
object togetServerSession
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;
};
- 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
- 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. 👻