如何使用Next构建一个完整的堆栈应用程序。js、Prisma、Postgres和Fastfy
在本文中,我们将学习如何使用Next构建完整的堆栈应用程序。js、Prisma、Postgres和Fastify。我们将构建一个考勤管理演示应用程序,用于管理员工的考勤。该应用程序的流程很简单:一个管理用户登录,创建当天的考勤表,然后每个员工在考勤表上登录和注销。
Next.js是什么?
下一个js是一个灵活的React框架,它为您提供了创建快速web应用程序的构建块。它通常被称为全堆栈React框架,因为它可以使前端和后端应用程序位于同一个代码基上,并使用无服务器功能。
Prisma是什么?
Prisma是一个开源的节点。js和Typescript ORM,大大简化了SQL数据库的数据建模、迁移和数据访问。在撰写本文时,Prisma支持以下数据库管理系统:PostgreSQL、MySQL、MariaDB、SQLite、AWS Aurora、Microsoft SQL Server、Azure SQL和MongoDB。您可能还想单击此处查看所有受支持的数据库管理系统的列表。
什么是Postgres?
Postgres也称为PostgreSQL,是一个免费的、开源的关系数据库管理系统。它是SQL语言的超集,具有许多特性,允许开发人员安全地存储和扩展复杂的数据工作负载。
先决条件
本教程是一个实践演示教程。因此,最好在计算机上安装以下软件:
-
Node.js安装在您的机器上
-
PostgreSQL数据库服务器正在运行
本教程的代码可以在Github上找到,所以请随意克隆并继续学习。
项目设置
让我们从设置下一个开始。js应用程序。要开始,请运行下面的命令。
npx create-next-app@latest
等待安装完成,然后运行下面的命令来安装依赖项。
yarn add fastify fastify-nextjs iron-session @prisma/client
yarn add prisma nodemon --dev
等待安装完成。
设置Next.js和Fastify
默认情况下,Next.js不使用Fastify作为其服务器。用Fastfy为我们的Next.js的应用程序,编辑包中的脚本字段。包含以下代码段的json文件。
"scripts": {
"dev": "nodemon server.js",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
创建Fastify服务器
现在让我们创建一个server.js文件。这个文件是我们应用程序的入口点,然后我们添加require(‘fastfy-nextjs’)以包括公开下一个的插件。Fastfy中的js API来处理渲染。
打开server.js文件,并添加以下代码段:
const fastify = require('fastify')()
async function noOpParser(req, payload) {
return payload;
}
fastify.register(require('fastify-nextjs')).after(() => {
fastify.addContentTypeParser('text/plain', noOpParser);
fastify.addContentTypeParser('application/json', noOpParser);
fastify.next('/*')
fastify.next('/api/*', { method: 'ALL' });
})
fastify.listen(3000, err => {
if (err) throw err
console.log('Server listening on <http://localhost:3000>')
})
在上面的代码片段中,我们使用了fastify-nextjs插件,该插件公开了下一个。Fastify中的js API为我们处理渲染。然后,我们用noOpParser函数解析传入的请求,该函数使请求体可供我们的下一个用户使用。我们使用[fastify.next]为我们的应用程序定义了两条路由(http://fastify.next命令。然后我们创建了Fastify服务器,让它监听端口3000。
现在,继续使用yarn-dev命令运行应用程序:应用程序将在localhost:3000上运行。
Prisma设置
首先,运行以下命令以获得基本的Prisma设置:
npx prisma init
上面的命令将创建一个带有模式的Prisma目录。prisma文件。这是您的主Prisma配置文件,其中将包含您的数据库模式。此外,一个。env文件将添加到项目的根目录中。打开。env文件,并将虚拟连接URL替换为PostgreSQL数据库的连接URL。
替换prisma/schema中的代码。prisma文件包含以下内容:
datasource db {
url = env("DATABASE_URL")
provider="postgresql"
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
email String @unique
name String
password String
role Role @default(EMPLOYEE)
attendance Attendance[]
AttendanceSheet AttendanceSheet[]
}
model AttendanceSheet {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy User? @relation(fields: [userId], references: [id])
userId Int?
}
model Attendance {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
signIn Boolean @default(true)
signOut Boolean
signInTime DateTime @default(now())
signOutTime DateTime
user User? @relation(fields: [userId], references: [id])
userId Int?
}
enum Role {
EMPLOYEE
ADMIN
}
在上面的代码片段中,我们创建了一个用户AttendanceSheet和Attention模型,定义了每个模型之间的关系。
接下来,在数据库中创建这些表。运行以下命令:
npx prisma db push
运行上述命令后,您应该会在终端中看到如下屏幕截图所示的输出:

创建实用程序函数
Prisma设置完成后,让我们创建三个实用功能,它们将不时在我们的应用程序中使用。
打开lib/parseBody。并添加以下代码段。此函数将请求正文解析为JSON:
export const parseBody = (body) => {
if (typeof body === "string") return JSON.parse(body)
return body
}
打开lib/request。并添加以下代码段。此函数发送POST请求。
export const sessionCookie = () => {
return ({
cookieName: "auth",
password: process.env.SESSION_PASSWORD,
// secure: true should be used in production (HTTPS) but can't be used in development (HTTP)
cookieOptions: {
secure: process.env.NODE_ENV === "production",
},
})
}
接下来,将SESSION_密码添加到.env文件:它应该是至少32个字符的字符串。
设计应用程序的样式
完成实用功能后,让我们为应用程序添加一些样式。我们正在为这个应用程序使用CSS模块,所以打开styles/Home。模块.css文件并添加以下代码段:
.container {
padding: 0 2rem;
}
.man {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.login {
width: 450px;
}
.login input {
width: 100%;
height: 50px;
margin: 4px;
}
.login button {
width: 100%;
height: 50px;
margin: 4px;
}
.dashboard {
display: grid;
grid-template-columns: 3fr 9fr;
grid-template-rows: 1fr;
grid-column-gap: 0px;
grid-row-gap: 0px;
height: calc(100vh - 60px);
}
.navbar {
height: 60px;
background-color: black;
}
创建边栏组件
造型完成后,让我们创建边栏组件,帮助我们导航到应用程序仪表板上的不同页面。打开组件/侧栏.js文件,并粘贴下面的代码段。
import Link from 'next/link'
import { useRouter } from 'next/router'
import styles from '../styles/SideBar.module.css'
const SideBar = () => {
const router = useRouter()
const logout = async () => {
try {
const response = await fetch('/api/logout', {
method: 'GET',
credentials: 'same-origin',
});
if(response.status === 200) router.push('/')
} catch (e) {
alert(e)
}
}
return (
<nav className={styles.sidebar}>
<ul>
<li> <Link href="/dashboard"> Dashboard</Link> </li>
<li> <Link href="/dashboard/attendance"> Attendance </Link> </li>
<li> <Link href="/dashboard/attendance-sheet"> Attendance Sheet </Link> </li>
<li onClick={logout}> Logout </li>
</ul>
</nav>
)
}
export default SideBar
登录页面
现在打开页面/索引.js文件,删除那里的所有代码并添加以下代码段。下面的代码将post请求与通过表单提供的电子邮件和密码一起发送到本地主机:3000/api/login route。一旦验证了凭据,它就会调用路由器。将用户重定向到localhost:3000/api/dashboard的push(“/dashboard”)方法:
import Head from 'next/head'
import { postData } from '../lib/request';
import styles from '../styles/Home.module.css'
import { useState } from 'react';
import { useRouter } from 'next/router'
export default function Home({posts}) {
const [data, setData] = useState({email: null, password: null});
const router = useRouter()
const submit = (e) => {
e.preventDefault()
if(data.email && data.password) {
postData('/api/login', data).then(data => {
console.log(data);
if (data.status === "success") router.push('/dashboard')
});
}
}
return (
<div className={styles.container}>
<Head>
<title>Login</title>
<meta name="description" content="Login" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<form className={styles.login}>
<input
type={"text"}
placeholder="Enter Your Email"
onChange={(e) => setData({...data, email: e.target.value})} />
<input
type={"password"}
placeholder="Enter Your Password"
onChange={(e) => setData({...data, password: e.target.value})} />
<button onClick={submit}>Login</button>
</form>
</main>
</div>
)
}
设置登录API路由
现在打开页面/api/login。并添加以下代码段。我们将使用PrismaClient进行数据库查询,withIronSessionApiRoute是在RESTful应用程序中处理用户会话的iron会话函数。
该路由处理对localhost:3000/api/login的登录后请求,并在用户经过身份验证后生成身份验证cookie。
import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';
export default withIronSessionApiRoute(
async function loginRoute(req, res) {
const { email, password } = parseBody(req.body)
const prisma = new PrismaClient()
// By unique identifier
const user = await prisma.user.findUnique({
where: {
email
},})
if(user.password === password) {
// get user from database then:
user.password = undefined
req.session.user = user
await req.session.save();
return res.send({ status: 'success', data: user });
};
res.send({ status: 'error', message: "incorrect email or password" });
},
sessionCookie(),
);
设置注销API路由
打开/page/api/logout文件并添加下面的代码段。此路由处理对本地主机的GET请求:3000/api/logout,该请求通过销毁会话cookie注销用户。
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from "../../lib/session";
export default withIronSessionApiRoute(
function logoutRoute(req, res, session) {
req.session.destroy();
res.send({ status: "success" });
},
sessionCookie()
);
创建仪表板页面
此页面为用户提供了登录和注销考勤表的界面。管理员还可以创建考勤表。打开页面/仪表板/索引.js文件,并在下面添加代码段。
import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { useState, useCallback } from "react";
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";
import { postData } from "../../lib/request";
export default function Page(props) {
const [attendanceSheet, setState] = useState(JSON.parse(props.attendanceSheet));
const sign = useCallback((action="") => {
const body = {
attendanceSheetId: attendanceSheet[0]?.id,
action
}
postData("/api/sign-attendance", body).then(data => {
if (data.status === "success") {
setState(prevState => {
const newState = [...prevState]
newState[0].attendance[0] = data.data
return newState
})
}
})
}, [attendanceSheet])
const createAttendance = useCallback(() => {
postData("/api/create-attendance").then(data => {
if (data.status === "success") {
alert("New Attendance Sheet Created")
setState([{...data.data, attendance:[]}])
}
})
}, [])
return (
<div>
<Head>
<title>Attendance Management Dashboard</title>
<meta name="description" content="dashboard" />
</Head>
<div className={styles.navbar}></div>
<main className={styles.dashboard}>
<SideBar />
<div className={dashboard.users}>
{
props.isAdmin && <button className={dashboard.create} onClick={createAttendance}>Create Attendance Sheet</button>
}
{ attendanceSheet.length > 0 &&
<table className={dashboard.table}>
<thead>
<tr>
<th>Id</th> <th>Created At</th> <th>Sign In</th> <th>Sign Out</th>
</tr>
</thead>
<tbody>
<tr>
<td>{attendanceSheet[0]?.id}</td>
<td>{attendanceSheet[0]?.createdAt}</td>
{
attendanceSheet[0]?.attendance.length != 0 ?
<>
<td>{attendanceSheet[0]?.attendance[0]?.signInTime}</td>
<td>{
attendanceSheet[0]?.attendance[0]?.signOut ?
attendanceSheet[0]?.attendance[0]?.signOutTime: <button onClick={() => sign("sign-out")}> Sign Out </button> }</td>
</>
:
<>
<td> <button onClick={() => sign()}> Sign In </button> </td>
<td>{""}</td>
</>
}
</tr>
</tbody>
</table>
}
</div>
</main>
</div>
)
}
我们使用getServerSideProps生成页面数据,withIronSessionSsr是用于处理服务器端呈现页面的iron会话函数。在下面的代码段中,我们使用考勤表中的一行查询考勤表的最后一行,其中userId等于存储在用户会话中的用户id。我们还检查用户是否是管理员。
export const getServerSideProps = withIronSessionSsr( async ({req}) => {
const user = req.session.user
const prisma = new PrismaClient()
const attendanceSheet = await prisma.attendanceSheet.findMany({
take: 1,
orderBy: {
id: 'desc',
},
include: {
attendance: {
where: {
userId: user.id
},
}
}
})
return {
props: {
attendanceSheet: JSON.stringify(attendanceSheet),
isAdmin: user.role === "ADMIN"
}
}
}, sessionCookie())
设置创建考勤API路由
打开页面/api/create Attention.js文件,并在下面添加代码段。
import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from '../../lib/session';
export default withIronSessionApiRoute( async function handler(req, res) {
const prisma = new PrismaClient()
const user = req.session.user
const attendanceSheet = await prisma.attendanceSheet.create({
data: {
userId: user.id,
},
})
res.json({status: "success", data: attendanceSheet});
}, sessionCookie())
设置签名考勤API路由
此路由处理我们对本地主机的API POST请求:3000/API/sign Attention。路由接受POST请求,而attendanceSheetId和action用于登录和注销attendanceSheet。
打开/页面/api/签名考勤.js文件,并在下面添加代码段。
import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';
export default withIronSessionApiRoute( async function handler(req, res) {
const prisma = new PrismaClient()
const {attendanceSheetId, action} = parseBody(req.body)
const user = req.session.user
const attendance = await prisma.attendance.findMany({
where: {
userId: user.id,
attendanceSheetId: attendanceSheetId
}
})
//check if atendance have been created
if (attendance.length === 0) {
const attendance = await prisma.attendance.create({
data: {
userId: user.id,
attendanceSheetId: attendanceSheetId,
signIn: true,
signOut: false,
signOutTime: new Date()
},
})
return res.json({status: "success", data: attendance});
} else if (action === "sign-out") {
await prisma.attendance.updateMany({
where: {
userId: user.id,
attendanceSheetId: attendanceSheetId
},
data: {
signOut: true,
signOutTime: new Date()
},
})
return res.json({status: "success", data: { ...attendance[0], signOut: true, signOutTime: new Date()}});
}
res.json({status: "success", data: attendance});
}, sessionCookie())
创建考勤页面
此服务器端呈现页面显示登录用户的所有考勤表。打开/页面/仪表板/考勤。js文件,并在下面添加代码段。
import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";
export default function Page(props) {
const data = JSON.parse(props.attendanceSheet)
return (
<div>
<Head>
<title>Attendance Management Dashboard</title>
<meta name="description" content="dashboard" />
</Head>
<div className={styles.navbar}></div>
<main className={styles.dashboard}>
<SideBar />
<div className={dashboard.users}>
<table className={dashboard.table}>
<thead>
<tr>
<th> Attendance Id</th> <th>Date</th>
<th>Sign In Time</th> <th>Sign Out Time</th>
</tr>
</thead>
<tbody>
{
data.map(data => {
const {id, createdAt, attendance } = data
return (
<tr key={id}>
<td>{id}</td> <td>{createdAt}</td>
{ attendance.length === 0 ?
(
<>
<td>You did not Sign In</td>
<td>You did not Sign Out</td>
</>
)
:
(
<>
<td>{attendance[0]?.signInTime}</td>
<td>{attendance[0]?.signOut ? attendance[0]?.signOutTime : "You did not Sign Out"}</td>
</>
)
}
</tr>
)
})
}
</tbody>
</table>
</div>
</main>
</div>
)
}
In the code snippet below, we query for all the rows from the attendanceSheet table and also fetch the attendance where the userId is equal to the user id stored in the user session.
export const getServerSideProps = withIronSessionSsr( async ({req}) => {
const user = req.session.user
const prisma = new PrismaClient()
const attendanceSheet = await prisma.attendanceSheet.findMany({
orderBy: {
id: 'desc',
},
include: {
attendance: {
where: {
userId: user.id
},
}
}
})
return {
props: {
attendanceSheet: JSON.stringify(attendanceSheet),
}
}
}, sessionCookie())
创建考勤表页面
此服务器端呈现的页面显示所有考勤表以及登录到该考勤表的员工。打开/页面/仪表板/考勤.js文件,并在下面添加代码段。
import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";
export default function Page(props) {
const data = JSON.parse(props.attendanceSheet)
return (
<div>
<Head>
<title>Attendance Management Dashboard</title>
<meta name="description" content="dashboard" />
</Head>
<div className={styles.navbar}></div>
<main className={styles.dashboard}>
<SideBar />
<div className={dashboard.users}>
{
data?.map(data => {
const {id, createdAt, attendance } = data
return (
<>
<table key={data.id} className={dashboard.table}>
<thead>
<tr>
<th> Attendance Id</th> <th>Date</th>
<th> Name </th> <th> Email </th> <th> Role </th>
<th>Sign In Time</th> <th>Sign Out Time</th>
</tr>
</thead>
<tbody>
{
(attendance.length === 0) &&
(
<>
<tr><td> {id} </td> <td>{createdAt}</td> <td colSpan={5}> No User signed this sheet</td></tr>
</>
)
}
{
attendance.map(data => {
const {name, email, role} = data.user
return (
<tr key={id}>
<td>{id}</td> <td>{createdAt}</td>
<td>{name}</td> <td>{email}</td>
<td>{role}</td>
<td>{data.signInTime}</td>
<td>{data.signOut ? attendance[0]?.signOutTime: "User did not Sign Out"}</td>
</tr>
)
})
}
</tbody>
</table>
</>
)
})
}
</div>
</main>
</div>
)
}
在下面的代码片段中,我们从attendanceSheet表中查询所有行,并通过选择名称、电子邮件和角色来获取考勤。
export const getServerSideProps = withIronSessionSsr(async () => {
const prisma = new PrismaClient()
const attendanceSheet = await prisma.attendanceSheet.findMany({
orderBy: {
id: 'desc',
},
include: {
attendance: {
include: {
user: {
select: {
name: true,
email: true,
role: true
}
}
}
},
},
})
return {
props: {
attendanceSheet: JSON.stringify(attendanceSheet),
}
}
}, sessionCookie())
测试应用程序
首先,我们必须向数据库中添加用户。我们将与普里斯玛工作室合作。要启动Prisma studio,请运行以下命令:
npx prisma studio
Prisma索引页面如下所示:

要创建具有管理员角色的数据库用户和具有员工角色的多个用户,请转到以下页面:

单击添加记录,然后填写所需字段:密码、名称、电子邮件和角色。完成后,单击绿色的Save 1 change(保存1更改)按钮。注意,为了简单起见,我们没有散列密码。
使用yarn dev启动服务器。这将启动服务器并在[localhost:3000]上运行应用程序(http://localhost:3000)登录页面如下所示。

请使用具有管理员角色的用户登录,因为只有管理员用户才能创建考勤表。登录成功后,应用程序会将您重定向到仪表板。
单击创建考勤表按钮创建考勤表,然后等待请求完成,考勤表将出现。用户仪表板如下所示。

考勤表如下图所示,单击“登录”按钮进行登录。登录成功后,将显示登录时间并显示注销按钮。单击注销按钮注销,并对不同的用户重复此过程多次。

接下来单击侧栏中的考勤链接以查看用户的考勤。结果应符合以下所示:

接下来单击侧栏上的考勤表链接,查看所有用户的考勤情况。结果如下:

结论
在本文中,您学习了如何使用Next.js自定义Fastify服务器。您还了解了Prisma和Prisma studio。我向您介绍了如何将Prisma连接到Postgres数据库,以及如何使用Prisma客户端和Prisma studio创建、读取和更新数据库。
您还学习了如何使用iron会话对用户进行身份验证。在本教程中,我们构建了一个完整的堆栈应用程序,使用Next管理员工考勤.js、Prisma、Postgres和Fastify。请确保继续关注更多内容,直到下一次。
原文标题:How to Build a Full-Stack App With Next.js, Prisma, Postgres, and Fastify
原文作者:Clara Ekekenta
原文链接:https://dzone.com/articles/how-to-build-a-full-stack-app-with-nextjs-prisma-postgres




