cover
Next.jsPrismaVercel postgres

Hướng dẫn Build Fullstack App với Next.js, Prisma, và Vercel Postgres

Cover
Screenshot 2023-07-29 at 22.12.52.png
Slug
build-fullstack-app-with-nextjs-prisma-vercel-postgres
Published
Published
Date
Jul 27, 2023
Category
Next.js
Prisma
Vercel postgres
Prisma là một next-generation ORM có thể được sử dụng để truy cập database bên trong các phần mềm dùng Node.js và typescript. Bên trong hướng dẫn này bạn sẽ học được cách thực hiện một ví dụ fullstack blog sử dụng các công nghệ dưới đây:
  • Next.js giống như React framework
  • Prisma làm ORM cho việc truy cập cũng như cập nhật database
  • NextAuth.js cho việc authentication thông qua GitHub (OAuth)
Bạn có thể tận dụng khả năng linh hoạt hiển thị của Next.js và cuối cùng có thể triển khai trực tiếp trên Vercel.

Điều kiện kiên quyết

Để hoàn thành tốt hướng dẫn này bạn sẽ cần:
  • Node.js
  • Một tài khoản Vercel để cài đặt Postgres database và deploy ứng dụng của bạn
  • Một tài khoản Github để tạo một ứng dụng OAuth

Bước 1: Set up dự án Next.js đầu tiên của bạn

Mở command line ở thu mục mà bạn sẽ setup dự án, bên trong terminal chạy lệnh bên dưới:
npx create-next-app --example https://github.com/prisma/blogr-nextjs-prisma/tree/main blogr-nextjs-prisma
Bây giờ bạn có thể đi tới dự án của mình và chạy nó:
cd blogr-nextjs-prisma && npm run dev
Bạn có thể mở ứng dụng của mình tại địa chỉ https://localhost:3000.
 
Đây là nhưng gì bạn có thể trông thấy ở thời điểm hiện tại:
notion image
 
Ứng dụng của bạn hiện tại đang hiển thị dữ liệu cố định được trả về từ getStaticProps bên trong file index.tsx. Trong suốt các phần tiếp theo bạn sẽ thay đổi điều này rằng data sẽ được trả về từ database thực sự.

Bước 2: Set up Vercel Postgres database của bạn

Đối với mục đích của hướng dẫn này, chúng ta sẽ sử dụng một Postgres database được hosted trên Vercel. Đầu tiên, đẩy repo của bạn đã clone ở bước một lên GitHub của chúng tôi và deploy nó lên Vercel để tạo một dự án Vercel.
Khi bạn có một Vercel project, chọn Storage tab, sau đó chọn Connect Database button. Dưới tab Create New chọn Postgres và sau đó chọn button Continue.
Để tạo mới một database, làm theo các bước phía dưới:
  1. Nhập sample_postgres_db (hoặc bất kì tên khác bạn mơ) tên database dưới Store Name. Tên có thể duy nhất bao gồm chữ cái alphanumeric, “_” và “-” và không thể vượt quá 32 kí tự.
  1. Chọn một region. Chúng tôi khuyến khí chọn các region (vùng) gần cùng chức năng của bạn nhất (mặc định là US East) để làm được độ trễ.
  1. Click Create.
Một database rỗng được tạo bên trong region được chỉ định. Bời vì bạn đã tạo Postgres database bên trong một dự án, chúng tôi tự động tạo và thêm biến môi trường đến dự án của bạn.
Cài đặt vercel CLI lên máy tính của bạn
npm i -g vercel@latest
Sau khi cài xong kéo biến môi trường cuối cùng để truy cập làm việc dự án local với postgres database.
vercel env pull .env.local
 
Chúng ta bây giờ có một cơ sở dữ liệu Vercel Postgres hoạt động đầy đủ và có tất cả các biến moi trường để chạy nó cục bộ và trên Vercel.

Bước 3: Cài đặt Prisma và tạo một database schema

Tiếp theo chúng ta sẽ cài đặt Prisma và kết nối nó tới PostgreSQL database của bạn. Bắt đầu bằng việc cài đặt Prisma CLI thông qua npm
npm install prisma --save-dev
Bạn sẽ tạo các bảng bên trong database sử dụng Prisma CLI.
Để làm điều này, tạo một thư mục prisma và thêm một file được gọi là schema.prisma, phần chính cấu hình Prisma của bạn sẽ bao gồm database schema của bạn.
Thêm một model được định nghĩa tới file schema.prisma và nó trông giống thế này:
// schema.prisma generator client { provider = "prisma-client-js" previewFeatures = ["jsonProtocol"] } 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") }
Note: bạn thỉnh thoảng dùng @map và @@map để map một số field và tên model đến các cột và các bảng khác nhau trong database. Điều này vì NextAuth.js đã có những cái yêu cầu chỉ định để call những thứ trong database theo một cách nhất định.
Schema này định nghĩa 2 model, mỗi một cái sẽ map với một bảng trong database: User và Post. Lưu ý rằng cũng có mối quan hệ (một-nhiều) giữa hai mô hình, thông qua trường author trên Post và trường posts trên User.
Để thực sự tạo bảng vào trong database, bạn bây giờ có thể sử dụng câu lệnh phía dưới của Prisma CLI
npx prisma db push
Nếu hoàn thành bạn sẽ thấy output như sau
Environment variables loaded from /Users/nikolasburk/Desktop/nextjs-guide/blogr-starter/.env.development.local Prisma schema loaded from prisma/schema.prisma 🚀 Your database is now in sync with your schema. Done in 2.10s
Nếu mà gặp lỗi như sau thì đổi tên biến .env.local thành .env và thử lại
notion image
Tới đây thì chúc mừng bạn đã tạo thành công các bảng vào trong database. Tiếp theo ta sẽ tạo data mẫu cho các bảng bằng cách sử dụng Prisma Studio. Chạy câu lệnh bên dưới:
npx prisma studio
Sử dụng giao diện Prisma Studio để tạo mới bản ghi User và Post và kết nối chúng thông qua các trường quan hệ.
notion image
notion image

Bước 4: Cài đặt và generate prisma client

Trước khi bạn có thể truy cập database của bạn từ Next.js sử dụng prisma, đầu tiên bạn phải cài đặt Prisma Client vào trong ứng dụng của bạn. Bạn có thể cài đặt thông qua câu lệnh sau:
npm install @prisma/client
Vì Prisma Client được điều chỉnh theo lược đồ của riêng bạn nên bạn cần cập nhật nó mỗi khi tệp lược đồ Prisma của bạn thay đổi bằng cách chạy lệnh sau:
npx prisma generate
Bạn sẽ sử dụng một phiên bản PrismaClient duy nhất mà bạn có thể nhập vào bất kỳ tệp nào nếu cần. Phiên bản sẽ được tạo trong tệp prisma.ts bên trong thư mục lib/ . Hãy tiếp tục và tạo thư mục và tệp bị thiếu:
mkdir lib && touch lib/prisma.ts
Bây giờ bạn thêm code phía dưới vào file prisma.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;
Bây giờ, bất cứ khi nào bạn cần truy cập đến database của bạn thì bạn có thể import prisma instance vào bên trong file bạn cần.

Bước 5: Cập nhật các view đã tồn tại để load data từ database

Các post feed được thực hiện bên trong file pages/index.tsx và chi tiết post view bên trong pages/p/[id].tsx hiện tại được hardcoded data. Bên trong bước này bạn sẽ điều chỉnh thực hiện để trả ra dữ liệu từ database sử dụng Prisma Client. Mở pages/index.tsx và thêm đoạn code phía dưới vào phần khai báo import
import prisma from '../lib/prisma';
import Prisma Client
prisma instance sẽ là interface của bạn với database khi bạn muốn đọc và viết data vào database. Bạn có thể ví dụ tạo mới một bản ghi User bằng cách gọi prisma.user.create() hoặc lấy toàn hộ bản ghi Post từ database với prisma.post.findMany(). Bạn có thể có cái nhìn tổng quá và toàn bộ Prisma Client API, ghé thăm Prisma docs.
Bây giờ bạn có thể ghi đè phần hardcode feed bên trong getStaticProps bên trong index.tsx với một lần gọi tới database riêng biệt.
export const getStaticProps: GetStaticProps = async () => { const feed = await prisma.post.findMany({ where: { published: true }, include: { author: { select: { name: true }, }, }, }); return { props: { feed }, revalidate: 10, }; };
Find all published posts in your database.
Có hai điều ghi chú về Prisma Client query:
  • Một filter where được chỉ định để lọc duy nhất các bản khi post với điều kiện published là true
  • name của author của bản ghi Post là được bắt buộc truy vấn và sẽ được trả ra.
Trước khi chạy ứng dụng của bạn, đi tới /pages/p/[id].tsx và điều chỉ thực thi ở đây bằng cách đọc chính xác bản ghi Post từ database.
Page này sử dụng getServerSideProps (SSR) thay vì getStaticProps (SSG). Điều này bởi vì dữ liệu là dữ liệu động, nó phụ thuộc vào id của post và được yêu cầu từ URL. Ví dụ view từ route /p/42 hiển thị Post có id là 42.
Giống như trước đây, đầu tiên bạn cần import Prisma Client vào pages/p/[id].tsx
import prisma from '../../lib/prisma';
Import your Prisma Client.
Bây giờ bạn có thể cập nhật triển khai của getServerSideProps để truy xuất bài đăng thích hợp từ cơ sở dữ liệu và cung cấp bài đăng đó cho giao diện người dùng của bạn thông qua props của components:
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, }; };
Tìm kiếm bài viết thông qua id
Và cuối cùng nếu ứng dụng của bạn không có thêm gì bạn có thể chạy lại nó với câu lệnh dưới đây
npm run dev
Nếu không thì lưu file và ở lại tại http://localhost:3000 trong ứng dụng của bạn. Bản ghi post sẽ được hiển thị giống như bên dưới
notion image
Bạn có thể click vào post để điều hướng tới màn hình detail

Bước 6: Set up GitHub authentication với NextAuth

Bên trong bước này bạn sẽ thêm GitHub authentication vào trong ứng dụng. Khi bạn có chức năng đó, bạn sẽ thêm nhiều tính năng hơn vào ứng dụng, chẳng hạn như người dùng được xác thực có thể tạo, xuất bản và xóa bài đăng thông qua giao diện.
Bước đầu tiên, cài đặt NextAuth.js vào ứng dụng của bạn:
npm install next-auth@4 @next-auth/prisma-adapter
Install the NextAuth library và the NextAuth Prisma Adapter.
Tiếp theo bạn cần thay đổi database schema của bạn để thêm các bảng được yêu cầu bởi NextAuth.
Thay đổi database schema, bạn có thể thay đổi thủ công file schema và run prisma db push lại. Mở schema.prisma và thay đổi nó như dưới đây:
// schema.prisma generator client { provider = "prisma-client-js" previewFeatures = ["jsonProtocol"] } datasource db { provider = "postgresql" url = env("POSTGRES_PRISMA_URL") // uses connection pooling directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection } // schema.prisma model Post { id String @id @default(cuid()) title String content String? published Boolean @default(false) author User? @relation(fields: [authorId], references: [id]) authorId String? } model Account { id String @id @default(cuid()) userId String @map("user_id") type String provider String providerAccountId String @map("provider_account_id") refresh_token String? access_token String? expires_at Int? token_type String? scope String? id_token String? session_state String? oauth_token_secret String? oauth_token String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) } model Session { id String @id @default(cuid()) sessionToken String @unique @map("session_token") userId String @map("user_id") expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? image String? posts Post[] accounts Account[] sessions Session[] } model VerificationToken { id Int @id @default(autoincrement()) identifier String token String @unique expires DateTime @@unique([identifier, token]) }
Cập nhật Prisma schema
Để học nhiều hơn về model này, ghé thăm trang tài liệu của NextAuth.js docs
Bây giờ bạn có thể điều chỉnh schema database của bạn bằng cách tạo các bảng thực sự vào trong database. Chạy đoạn lệnh dưới đây:
npx prisma db push
Cập nhật các bảng vào trong database của bạn
Vì bạn sử dụng Github authentication, bạn cần tạo một OAuth app on GitHub. Đầu tiên, đăng nhập vào tài khoản GitHub của bạn. Sau đó truy cập vào Settings , sau đó mở Developer Setting, sau đó chuyển sang OAuth Apps.
Tạo một ứng dụng OAuth bên trong GitHub
Tạo một ứng dụng OAuth bên trong GitHub
Khi đăng kí bạn cần phải điền một số thông tin cho ứng dụng của bạn.. Authorization callback URL sẽ là /api/auth route: http://localhost:3000/api/auth
Một điều quan trọng là Authorization callback URL trường này duy nhất hỗ trợ một URL. có nghĩ là nếu bạn muốn deploy ứng dụng của bạn sau này với một URL prod, bạn sẽ cần cài lại một cái mới.
Chắc chắn rằng Authorization callback URL là chính xác
Chắc chắn rằng Authorization callback URL là chính xác
Click vào Register application, và bạn sẽ có thể tìm thấy ClientId và Client Secret. Copy và pate thông tin vào trong .env file bên trong thư mục root của dự án với tên là GITHUB_ID và GITHUB_SECRET. Đặt NEXTAUTH_URL có giá trị giống với Authorization callback URL được cấu hình trên github.
# .env # GitHub OAuth GITHUB_ID=6bafeb321963449bdf51 GITHUB_SECRET=509298c32faa283f28679ad6de6f86b2472e1bff NEXTAUTH_URL=http://localhost:3000/api/auth
ví dụ cho .env hoàn thiện
Bạn sẽ cần duy trì trạng thái xác thực của người dùng trên toàn bộ ứng dụng. Tạo nhanh thay đổi trong ứng dụng của bạn ở file _app.tsx và bọc root component với một SessionProvider từ next-auth/react. Mở file và ghi đè nội dung hiện tại với code dưới đây.
import { SessionProvider } from 'next-auth/react'; import { AppProps } from 'next/app'; const App = ({ Component, pageProps }: AppProps) => { return ( <SessionProvider session={pageProps.session}> <Component {...pageProps} /> </SessionProvider> ); }; export default App;
Bọc ứng dụng của bạn với NextAuth SessionProvider

Bước 7: Thêm chức năng Login

Nút đăng nhập và một vài cái UI components sẽ được thêm vào file Header.tsx. mở file và thêm code sau vào nó.
import React from 'react'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { signOut, useSession } from 'next-auth/react'; const Header: React.FC = () => { const router = useRouter(); const isActive: (pathname: string) => boolean = (pathname) => router.pathname === pathname; const { data: session, status } = useSession(); let left = ( <div className="left"> <Link href="/"> <a className="bold" data-active={isActive('/')}> Feed </a> </Link> <style jsx>{` .bold { font-weight: bold; } a { text-decoration: none; color: var(--geist-foreground); display: inline-block; } .left a[data-active='true'] { color: gray; } a + a { margin-left: 1rem; } `}</style> </div> ); let right = null; if (status === 'loading') { left = ( <div className="left"> <Link href="/"> <a className="bold" data-active={isActive('/')}> Feed </a> </Link> <style jsx>{` .bold { font-weight: bold; } a { text-decoration: none; color: var(--geist-foreground); display: inline-block; } .left a[data-active='true'] { color: gray; } a + a { margin-left: 1rem; } `}</style> </div> ); right = ( <div className="right"> <p>Validating session ...</p> <style jsx>{` .right { margin-left: auto; } `}</style> </div> ); } if (!session) { right = ( <div className="right"> <Link href="/api/auth/signin"> <a data-active={isActive('/signup')}>Log in</a> </Link> <style jsx>{` a { text-decoration: none; color: var(--geist-foreground); display: inline-block; } a + a { margin-left: 1rem; } .right { margin-left: auto; } .right a { border: 1px solid var(--geist-foreground); padding: 0.5rem 1rem; border-radius: 3px; } `}</style> </div> ); } if (session) { left = ( <div className="left"> <Link href="/"> <a className="bold" data-active={isActive('/')}> Feed </a> </Link> <Link href="/drafts"> <a data-active={isActive('/drafts')}>My drafts</a> </Link> <style jsx>{` .bold { font-weight: bold; } a { text-decoration: none; color: var(--geist-foreground); display: inline-block; } .left a[data-active='true'] { color: gray; } a + a { margin-left: 1rem; } `}</style> </div> ); right = ( <div className="right"> <p> {session.user.name} ({session.user.email}) </p> <Link href="/create"> <button> <a>New post</a> </button> </Link> <button onClick={() => signOut()}> <a>Log out</a> </button> <style jsx>{` a { text-decoration: none; color: var(--geist-foreground); display: inline-block; } p { display: inline-block; font-size: 13px; padding-right: 1rem; } a + a { margin-left: 1rem; } .right { margin-left: auto; } .right a { border: 1px solid var(--geist-foreground); padding: 0.5rem 1rem; border-radius: 3px; } button { border: none; } `}</style> </div> ); } return ( <nav> {left} {right} <style jsx>{` nav { display: flex; padding: 2rem; align-items: center; } `}</style> </nav> ); }; export default Header;
Cho phép người dùng đăng nhập thông qua Header
Đây và tổng quan làm thế nào header được render
  • nếu không có người dùng nào được xác thực, một nút login sẽ được hiển thị.
  • nếu người dùng được xác thực, các nút khác như tạo mới post và đăng xuất sẽ được hiển thị.
Bạn đã sẵn sàng chạy ứng dụng xác thực của bạn bằng npm run dev. Bạn sẽ tìm thấy button login được hiển thị bây giờ. Tuy nhiên nếu bạn click vào nó thì nó sẽ điều hướng bạn tới http://localhost:3000/api/auth/signin nhưng Next.js sẽ render một page 404 cho bạn.
Điều này sảy ra là bởi vì NextAuth.js yêu cầu bạn phải cài route chỉ định cho việc authentication. Bạn sẽ làm điều này sau.
Tạo một thư mục và thêm 1 file mới bên trong pages/api:
mkdir -p pages/api/auth && touch pages/api/auth/[...nextauth].ts
Tạo mới một thư mục và API route
Bên trong file mới này, bạn bây giờ cần thêm cấu hình cho NextAuth.js với GitHub OAth và Prisma adapter
import { NextApiHandler } from 'next'; import NextAuth from 'next-auth'; import { PrismaAdapter } from '@next-auth/prisma-adapter'; import GitHubProvider from 'next-auth/providers/github'; import prisma from '../../../lib/prisma'; const authHandler: NextApiHandler = (req, res) => NextAuth(req, res, options); export default authHandler; const options = { providers: [ GitHubProvider({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET, }), ], adapter: PrismaAdapter(prisma), secret: process.env.SECRET, };
Cấu hình NextAuth, bao gồm Prisma Adapter.
Khi thêm code xong bạn có thể truy cập http://localhost:3000/api/auth/signin một lần nữa. Và button Sign in with GitHub được hiển thị
notion image
Nếu bạn click vào nó, bạn sẽ chuyển tiếp đến GitHub, nơi đây bạn có thể đăng nhập với GitHub. Sau khi xác thực hoàn thành bạn sẽ được chuyển quay lại ứng dụng
Note: Nếu bạn thấy lỗi và không thể đăng nhập được, stop ứng dụng và run lại bằng câu lện npm run dev
Lúc này giao diện header sẽ được thay đổi và hiển thị button cho authenticated users.
Phần header hiển thị một button đăng xuất
Phần header hiển thị một button đăng xuất

Bài viết liên quan

+84 387170123
him98dev@gmail.com
Cau Giay, Ha Noi
Vietnam.