Typescript 🟦 CRUD API:Next.js、Tailwind、tRPC、Prisma Postgres、Docker
在本文结束时,您将了解如何使用以下技术构建一个完整的 CRUD API 应用程序,包括一个简单的前端来使用它:
- Next.js
- TypeScript
- Tailwind CSS
- tRPC
- 棱镜
- Postgres
- Docker
技术种类繁多,但我们会尽量简化示例,使其易于理解。我们还将使用 Zod,一个验证库。
如果您更喜欢视频版本:
所有代码均可在GitHub上免费获取(链接见视频说明)。
让我们开始吧。
🏁 简介
以下是我们要创建的应用程序架构图:
我们将为基本的 CRUD 操作创建五个端点:
创建
全部读取
读取一个
更新
删除
我们将使用 Create T3 App 创建应用程序,并将该应用程序连接到在 docker 容器中运行的 Postgres 实例。
👣 步骤
我们将提供一步一步的指南,以便您可以跟随。
步骤如下:
- 先决条件
- 项目创建和依赖项安装
- 使用 Docker 运行 Postgres 数据库
- 配置 Prisma 和数据库模式
- 编写 tRPC 程序
- 在 index.tsx 文件中配置处理程序
- 使用 Tailwind 编写简单的前端应用程序
💡 先决条件
要求:
- Node.js
- Docker
- 任何编辑器(我将使用 VS Code)
您可以通过输入以下命令检查 Node 和 Docker 版本:
node --version
docker --version
我建议使用最新版本。
🚀 使用 Create T3 App 创建新项目
为了创建我们的应用程序,我们将使用“Create T3 App”。它是一个 CLI 工具,用于创建一个包含所有必要技术的新项目。
npm create t3-app@latest
除 nextauth 之外全部选择(使用空格键选择/取消选择)
这通常需要几分钟。
然后进入目录:
cd my-t3-app
现在使用任意 IDE 打开项目。
如果您使用 VS Code,则可以输入:
code .
该项目应该看起来与此类似。
我们可以通过输入以下内容在本地测试该应用程序:
npm run dev
并参观localhost:3000
🐳 使用 Docker 运行 Postgres 容器
让我们使用 Docker 运行一个 Postgres 容器。
为此,docker-compose.yml
在项目根级别创建一个名为 的新文件。
然后用以下代码填充文件:
version: "3.9"
services:
db:
container_name: db
image: postgres:12
ports:
- "5432:5432"
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_USER=postgres
- POSTGRES_DB=postgres
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata: {}
解释:
-
db
是我们将要运行的单个服务(容器)的名称 -
container_name
是我们使用的自定义名称。在本例中,它是db
-
image
是我们从 DockerHub 使用的镜像。我们将使用 Postgres 版本 12 -
ports
是容器的外部-内部端口映射。为了避免混淆,我们将使用 Postgres 的默认端口映射。 -
environment
是定义环境变量:我们将使用“postgres”作为用户、密码和数据库(不要在生产中这样做!) -
volumes
是声明我们想要在此服务中使用的卷。我们使用一个命名卷,该卷的定义如下。
现在让我们使用以下命令运行这个容器:
docker compose up -d
让我们检查容器是否已启动并正在运行:
docker ps -a
现在我们准备将应用程序连接到数据库并创建数据库模式
🔼 使用 Prisma 将应用程序连接到 Postgres 数据库
要将我们的应用程序连接到数据库,请打开该.env
文件并将内容替换为以下内容:
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
该文件将被添加到.gitignore 并且不会被推送到公共存储库,因此如果您克隆了该项目,则必须创建此文件。
现在打开并编辑该prisma/schema.prisma
文件,将其替换为以下内容:
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
previewFeatures = ["jsonProtocol"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
//User with an id, name and email as strings
model User {
id String @id @default(uuid())
name String
email String
}
我们将提供者替换为用户postgresql
,并将模型替换为用户:
- 一个 ID
- 一个名字
- 一封电子邮件
所有字段都是字符串。
现在,要更新数据库中的模式,您可以输入:
npx prisma migrate dev --name init
为了测试一切是否正常,我们可以使用 Prisma Studio(Prisma 自带的工具)。输入:
npx prisma studio
并打开localhost:5555
然后您可以手动添加记录。这以后会派上用场。
__
⌨️ 编写 Next.js 应用程序
📜 example.ts 文件
现在我们已经设置了数据库并连接了我们的应用程序,我们可以开始编写一些代码。
打开src/server/api/routers/example.ts
文件并将内容替换为以下内容:
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
const idSchema = z.object({ id: z.string() });
const userSchema = z.object({
name: z.string(),
email: z.string(),
});
const userUpdateSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string(),
});
export const exampleRouter = createTRPCRouter({
//get all users
getAll: publicProcedure.query(({ ctx }) => {
return ctx.prisma.user.findMany();
}),
//get user by id
getOne: publicProcedure
.input(idSchema)
.query(({ input, ctx }) => {
return ctx.prisma.user.findUnique({
where: idSchema.parse(input),
});
}),
//create user
createUser: publicProcedure
.input(userSchema)
.mutation(({ input, ctx }) => {
return ctx.prisma.user.create({
data: userSchema.parse(input),
});
}),
//update user
updateUser: publicProcedure
.input(userUpdateSchema)
.mutation(({ input, ctx }) => {
return ctx.prisma.user.update({
where: {
id: input.id.toString(),
},
data: userUpdateSchema.parse(input),
});
}),
//delete user
deleteUser: publicProcedure
.input(idSchema)
.mutation(({ input, ctx }) => {
return ctx.prisma.user.delete({
where: idSchema.parse(input),
});
}),
});
解释:
-
z
是一个用于验证函数输入和输出的库。我们将使用它来验证函数的输入和输出。 -
createTRPCRouter
是一个为我们创建路由器的函数。它接收一个包含我们想要公开的函数的对象作为参数。 -
publicProcedure
是一个接受模式并返回接受函数的函数的函数。 -
idSchema
是一种将具有 id 的对象作为字符串的模式。 -
userSchema
是一种以名称和电子邮件作为字符串的对象模式。 -
userUpdateSchema
是一种将具有 ID、名称和电子邮件的对象作为字符串的模式。 -
exampleRouter
是我们将用来公开功能的路由器。 -
getAll
是一个返回所有用户的函数。 -
getOne
是一个通过id返回一个用户的函数。 -
createUser
是一个创建用户的函数。 -
updateUser
是更新用户的功能。 -
deleteUser
是删除用户的功能。
如需进一步解释,请查看:https://youtu.be/Gf9RkaHnsR8? t=406
📜 index.tsx 文件
我们只需要再编辑一个文件。
打开src/pages/index.tsx
并填充以下内容:
import { useState } from "react";
import { api } from "~/utils/api";
export default function Home() {
//define constants
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [nameToUpdate, setNameToUpdate] = useState("");
const [emailToUpdate, setEmailToUpdate] = useState("");
const [userId, setUserId] = useState("");
const [userIdToUpdate, setUserIdToUpdate] = useState("");
const [userIdToDelete, setUserIdToDelete] = useState("");
//define functions
const fetchAllUsers = api.example.getAll.useQuery();
const fetchOneUser = api.example.getOne.useQuery({ id: userId });
const createUserMutation = api.example.createUser.useMutation();
const updateUserMutation = api.example.updateUser.useMutation();
const deleteUserMutation = api.example.deleteUser.useMutation();
//define handlers
const handleCreateUser = async () => {
try {
await createUserMutation.mutateAsync({
name: name,
email: email,
});
setName("");
setEmail("");
fetchAllUsers.refetch();
} catch (error) {
console.log(error);
}
};
const handleUpdateUser = async () => {
try {
await updateUserMutation.mutateAsync({
id: userIdToUpdate,
name: nameToUpdate,
email: emailToUpdate,
});
setNameToUpdate("");
setEmailToUpdate("");
setUserIdToUpdate("");
fetchAllUsers.refetch();
} catch (error) {
console.log(error);
}
};
const handleDeleteUser = async () => {
try {
await deleteUserMutation.mutateAsync({
id: userIdToDelete,
});
setUserIdToDelete("");
fetchAllUsers.refetch();
} catch (error) {
console.log(error);
}
};
//return an empty div
return (
<div className="mx-auto p-8">
<div className="mb-8">
<h2 className="mb-4 text-2xl font-bold">Get All Users</h2>
</div>
<button
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
onClick={() => fetchAllUsers.refetch()}
>
Get All Users
</button>
<div className="text- mb-4 mt-4 grid grid-cols-3 gap-4 font-bold">
<p>Id</p>
<p>Name</p>
<p>Email</p>
</div>
{fetchAllUsers.data &&
fetchAllUsers.data.map((user) => (
<div
key={user.id}
className="my-4 grid grid-cols-3 gap-4 rounded border border-gray-300 bg-white p-4 shadow"
>
<p>{user.id}</p>
<p>{user.name}</p>
<p>{user.email}</p>
</div>
))}
{/* Get one user UI */}
<div className="mb-8">
<h2 className="mb-4 text-2xl font-bold">Get One User</h2>
<div className="mb-4 flex">
<input
className="mr-2 border border-gray-300 p-2"
placeholder="Enter user id to get"
value={userId || ""}
onChange={(e) => setUserId(String(e.target.value))}
/>
<button
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
onClick={() => fetchOneUser.refetch()}
>
Get One User
</button>
</div>
{fetchOneUser.data && (
<div>
<p>Name: {fetchOneUser.data.name}</p>
<p>Email: {fetchOneUser.data.email}</p>
</div>
)}
</div>
{/* Create User */}
<div className="mb-8">
<h2 className="mb-4 text-2xl font-bold">Create New User</h2>
<div className="mb-4 flex">
<input
className="mr-2 w-1/2 border border-gray-300 p-2"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
className="w-1/2 border border-gray-300 p-2"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<button
className="rounded bg-green-500 px-4 py-2 text-white hover:bg-green-600"
onClick={handleCreateUser}
>
Create User
</button>
</div>
{/* Update User */}
<div className="mb-8">
<h2 className="mb-4 text-2xl font-bold">Update User</h2>
<div className="mb-4 flex">
<input
className="mr-2 w-1/2 border border-gray-300 p-2"
placeholder="Name to update"
value={nameToUpdate}
onChange={(e) => setNameToUpdate(e.target.value)}
/>
<input
className="w-1/2 border border-gray-300 p-2"
placeholder="Email to update"
value={emailToUpdate}
onChange={(e) => setEmailToUpdate(e.target.value)}
/>
</div>
<input
placeholder="Enter user id to update"
className="mr-2 border border-gray-300 p-2"
value={userIdToUpdate}
onChange={(e) => setUserIdToUpdate(e.target.value)}
/>
<button
className="mt-2 rounded bg-orange-500 px-4 py-2 text-white hover:bg-orange-600"
onClick={handleUpdateUser}
>
Update User
</button>
</div>
{/* Delete User */}
<div className="mb-8">
<h2 className="mb-4 text-2xl font-bold">Delete User</h2>
<input
placeholder="Enter user id to delete"
className="mr-2 border border-gray-300 p-2"
value={userIdToDelete}
onChange={(e) => setUserIdToDelete(e.target.value)}
/>
<button
className="mt-2 rounded bg-red-500 px-4 py-2 text-white hover:bg-red-600"
onClick={handleDeleteUser}
>
Delete User
</button>
</div>
</div>
);
}
解释:
-
我们使用 Next.js 创建一个单页应用程序,使用
useRouter
钩子从 URL 获取查询参数。 -
我们使用
useMutation
钩子来react-query
创建、更新和删除用户。 -
我们使用
useQuery
钩子来react-query
获取所有用户并获取一个用户。 -
我们有五个函数来处理 CRUD 操作:
-
getAllUsers
- 获取所有用户 -
getOneUser
- 获取一个用户 -
handleCreateUser
- 创建新用户 -
handleUpdateUser
- 更新用户 -
handleDeleteUser
- 删除用户
我们为每个使用 Tailwind CSS 样式的功能创建一个 UI。
每次操作后,我们都会重新获取数据以查看变化。
例如,创建新用户后,我们重新获取所有用户以在列表中看到新用户。
如需进一步解释,请观看视频:https://youtu.be/Gf9RkaHnsR8
我们的最终项目应该是这样的:
该项目旨在作为示例,以便您可以将其作为项目的起点。如果您想做出贡献,请随时在GitHub上创建 PR (链接见视频说明)。
__
🏁 结论
我们使用以下技术构建了 CRUD API:
- Next.js
- TypeScript
- Tailwind CSS
- tRPC
- 棱镜
- Postgres
- Docker
如果您更喜欢视频版本:
所有代码均可在GitHub上免费获取(链接见视频说明)。
您可以在这里联系Francesco
鏂囩珷鏉ユ簮锛�https://dev.to/francescoxx/typescript-crud-api-with-trpc-4689