Typescript 🟦 CRUD API:Next.js、Tailwind、tRPC、Prisma Postgres、Docker

2025-06-11

Typescript 🟦 CRUD API:Next.js、Tailwind、tRPC、Prisma Postgres、Docker

在本文结束时,您将了解如何使用以下技术构建一个完整的 CRUD API 应用程序,包括一个简单的前端来使用它:

  • Next.js
  • TypeScript
  • Tailwind CSS
  • tRPC
  • 棱镜
  • Postgres
  • Docker

技术种类繁多,但我们会尽量简化示例,使其易于理解。我们还将使用 Zod,一个验证库。

如果您更喜欢视频版本:

所有代码均可在GitHub上免费获取(链接见视频说明)。

让我们开始吧。


🏁 简介

以下是我们要创建的应用程序架构图:

Typescript CRUD API:Next.js、TypeScript、Tailwind、tRPC、Prisma Postgres、Docker

我们将为基本的 CRUD 操作创建五个端点:

创建
全部读取
读取一个
更新
删除

我们将使用 Create T3 App 创建应用程序,并将该应用程序连接到在 docker 容器中运行的 Postgres 实例。


👣 步骤

我们将提供一步一步的指南,以便您可以跟随。

步骤如下:

  1. 先决条件
  2. 项目创建和依赖项安装
  3. 使用 Docker 运行 Postgres 数据库
  4. 配置 Prisma 和数据库模式
  5. 编写 tRPC 程序
  6. 在 index.tsx 文件中配置处理程序
  7. 使用 Tailwind 编写简单的前端应用程序

💡 先决条件

要求:

  • Node.js
  • Docker
  • 任何编辑器(我将使用 VS Code)

您可以通过输入以下命令检查 Node 和 Docker 版本:



node --version


Enter fullscreen mode Exit fullscreen mode


docker --version


Enter fullscreen mode Exit fullscreen mode

我建议使用最新版本。

Typescript CRUD API:Next.js、TypeScript、Tailwind、tRPC、Prisma Postgres、Docker


🚀 使用 Create T3 App 创建新项目

为了创建我们的应用程序,我们将使用“Create T3 App”。它是一个 CLI 工具,用于创建一个包含所有必要技术的新项目。

Typescript CRUD API:Next.js、TypeScript、Tailwind、tRPC、Prisma Postgres、Docker



npm create t3-app@latest


Enter fullscreen mode Exit fullscreen mode

除 nextauth 之外全部选择(使用空格键选择/取消选择)

Typescript CRUD API:Next.js、TypeScript、Tailwind、tRPC、Prisma Postgres、Docker

这通常需要几分钟。

然后进入目录:



cd my-t3-app


Enter fullscreen mode Exit fullscreen mode

现在使用任意 IDE 打开项目。
如果您使用 VS Code,则可以输入:



code .


Enter fullscreen mode Exit fullscreen mode

该项目应该看起来与此类似。

Typescript CRUD API:Next.js、TypeScript、Tailwind、tRPC、Prisma Postgres、Docker

我们可以通过输入以下内容在本地测试该应用程序:



npm run dev


Enter fullscreen mode Exit fullscreen mode

并参观localhost:3000

Typescript CRUD API:Next.js、TypeScript、Tailwind、tRPC、Prisma Postgres、Docker


🐳 使用 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: {}


Enter fullscreen mode Exit fullscreen mode

解释:

  • db是我们将要运行的单个服务(容器)的名称

  • container_name是我们使用的自定义名称。在本例中,它是db

  • image是我们从 DockerHub 使用的镜像。我们将使用 Postgres 版本 12

  • ports是容器的外部-内部端口映射。为了避免混淆,我们将使用 Postgres 的默认端口映射。

  • environment是定义环境变量:我们将使用“postgres”作为用户、密码和数据库(不要在生产中这样做!)

  • volumes是声明我们想要在此服务中使用的卷。我们使用一个命名卷,该卷的定义如下。

现在让我们使用以下命令运行这个容器:



docker compose up -d


Enter fullscreen mode Exit fullscreen mode

让我们检查容器是否已启动并正在运行:



docker ps -a


Enter fullscreen mode Exit fullscreen mode

Typescript CRUD API:Next.js、TypeScript、Tailwind、tRPC、Prisma Postgres、Docker

现在我们准备将应用程序连接到数据库并创建数据库模式


🔼 使用 Prisma 将应用程序连接到 Postgres 数据库

要将我们的应用程序连接到数据库,请打开该.env文件并将内容替换为以下内容:



DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"


Enter fullscreen mode Exit fullscreen mode

该文件将被添加到.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
}


Enter fullscreen mode Exit fullscreen mode

我们将提供者替换为用户postgresql,并将模型替换为用户:

  • 一个 ID
  • 一个名字
  • 一封电子邮件

所有字段都是字符串。

现在,要更新数据库中的模式,您可以输入:



npx prisma migrate dev --name init


Enter fullscreen mode Exit fullscreen mode

为了测试一切是否正常,我们可以使用 Prisma Studio(Prisma 自带的工具)。输入:



npx prisma studio


Enter fullscreen mode Exit fullscreen mode

并打开localhost:5555

然后您可以手动添加记录。这以后会派上用场。

Typescript CRUD API:Next.js、TypeScript、Tailwind、tRPC、Prisma Postgres、Docker

__

⌨️ 编写 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),
      });
    }),
});


Enter fullscreen mode Exit fullscreen mode

解释:

  • 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>
  );
}


Enter fullscreen mode Exit fullscreen mode

解释:

  • 我们使用 Next.js 创建一个单页应用程序,使用useRouter钩子从 URL 获取查询参数。

  • 我们使用useMutation钩子来react-query创建、更新和删除用户。

  • 我们使用useQuery钩子来react-query获取所有用户并获取一个用户。

  • 我们有五个函数来处理 CRUD 操作:

  • getAllUsers- 获取所有用户

  • getOneUser- 获取一个用户

  • handleCreateUser- 创建新用户

  • handleUpdateUser- 更新用户

  • handleDeleteUser- 删除用户

我们为每个使用 Tailwind CSS 样式的功能创建一个 UI。

每次操作后,我们都会重新获取数据以查看变化。

例如,创建新用户后,我们重新获取所有用户以在列表中看到新用户。

如需进一步解释,请观看视频:https://youtu.be/Gf9RkaHnsR8

我们的最终项目应该是这样的:

Typescript CRUD API:Next.js、TypeScript、Tailwind、tRPC、Prisma Postgres、Docker

该项目旨在作为示例,以便您可以将其作为项目的起点。如果您想做出贡献,请随时在GitHub上创建 PR (链接见视频说明)。

__

🏁 结论

我们使用以下技术构建了 CRUD API:

  • Next.js
  • TypeScript
  • Tailwind CSS
  • tRPC
  • 棱镜
  • Postgres
  • Docker

如果您更喜欢视频版本:

所有代码均可在GitHub上免费获取(链接见视频说明)。

您可以在这里联系Francesco

鏂囩珷鏉ユ簮锛�https://dev.to/francescoxx/typescript-crud-api-with-trpc-4689
PREV
RxJS 模式:效率和性能
NEXT
使用 Flask、SQLAlchemy、Postgres、Docker、Docker Compose 的 Python CRUD Rest API