Typescript CRUD API: Next.js، Tailwind، tRPC، Prisma Postgres، Docker
در پایان این مقاله، نحوه ساخت یک برنامه FULL CRUD API، از جمله یک ظاهر ساده برای استفاده از آن، با استفاده از فناوریهای زیر را خواهید فهمید:
- Next.js
- TypeScript
- Tailwind CSS
- tRPC
- پریسما
- Postgres
- داکر
آنها فن آوری های زیادی هستند، اما ما مثال را تا حد امکان اساسی نگه می داریم تا قابل درک باشد. ما همچنین از Zod، یک کتابخانه اعتبار سنجی استفاده خواهیم کرد.
اگر نسخه ویدیویی را ترجیح می دهید:
https://www.youtube.com/watch?v=Gf9RkaHnsR8
همه کدها به صورت رایگان در GitHub در دسترس هستند (لینک در توضیحات ویدیو).
بیا شروع کنیم.
🏁 معرفی
در اینجا طرحی از معماری برنامه ای که می خواهیم ایجاد کنیم آمده است:
ما پنج نقطه پایانی برای عملیات اصلی CRUD ایجاد خواهیم کرد:
ايجاد كردن
همه اش را بخوان
یکی را بخوانید
به روز رسانی
حذف
ما برنامه را با استفاده از Create T3 App ایجاد می کنیم، و این برنامه را به یک نمونه Postgres که در یک ظرف داکر اجرا می شود متصل می کنیم.
👣 مراحل
ما با یک راهنمای گام به گام همراه می شویم تا بتوانید آن را دنبال کنید.
در اینجا مراحل انجام می شود:
- پیش نیازها
- ایجاد پروژه و نصب وابستگی
- پایگاه داده Postgres را با Docker اجرا کنید
- Prisma و شمای پایگاه داده را پیکربندی کنید
- رویه های tRPC را بنویسید
- کنترل کننده ها را در فایل index.tsx پیکربندی کنید
- برنامه ساده ظاهری را با استفاده از Tailwind بنویسید
💡 پیش نیازها
الزامات:
- Node.js
- داکر
- هر ویرایشگر (من از کد VS استفاده خواهم کرد)
می توانید نسخه های Node و Docker را با تایپ کردن بررسی کنید:
node --version
docker --version
پیشنهاد می کنم از آخرین نسخه ها استفاده کنید.
🚀 با استفاده از برنامه Create T3 یک پروژه جدید ایجاد کنید
برای ایجاد برنامه خود، از “Create T3 App” استفاده می کنیم. این یک ابزار CLI برای ایجاد یک پروژه جدید با تمام فناوری های لازم است.
npm create t3-app@latest
همه را انتخاب کنید به جز nextauth (از فاصله برای انتخاب/لغو انتخاب استفاده کنید)
این معمولا چند دقیقه طول می کشد.
سپس وارد دایرکتوری شوید:
cd my-t3-app
اکنون پروژه را با استفاده از هر IDE باز کنید.
اگر از VS Code استفاده می کنید، می توانید تایپ کنید:
code .
پروژه باید شبیه به این باشد.
ما می توانیم برنامه را به صورت محلی با تایپ کردن آزمایش کنیم:
npm run dev
و بازدید کنید localhost:3000
🐳 ظرف Postgres را با استفاده از Docker اجرا کنید
بیایید یک ظرف Postgres را با استفاده از Docker اجرا کنیم.
برای این کار یک فایل جدید به نام ایجاد کنید 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 one پیش فرض استفاده می کنیم. -
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
و مدل با کاربر با:
همه فیلدها رشته هستند.
اکنون، برای به روز رسانی طرحواره در DB، می توانید تایپ کنید:
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
طرحی است که یک شی با شناسه، نام و ایمیل را به عنوان رشته می گیرد. -
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 استایل شده است یک رابط کاربری ایجاد می کنیم.
پس از هر عملیات، داده ها را دوباره واکشی می کنیم تا تغییرات را ببینیم.
به عنوان مثال، پس از ایجاد یک کاربر جدید، همه کاربران را دوباره واکشی می کنیم تا کاربر جدید را در لیست ببینند.
برای توضیح بیشتر، ویدیو را ببینید: https://youtu.be/Gf9RkaHnsR8
پروژه نهایی ما باید به این صورت باشد:
این پروژه به عنوان نمونه در نظر گرفته شده است تا بتوانید از آن به عنوان نقطه شروع برای پروژه های خود استفاده کنید. اگر میخواهید مشارکت داشته باشید، با خیال راحت یک روابط عمومی در GitHub باز کنید (لینک در توضیحات ویدیو).
__
🏁 نتیجه گیری
ما یک CRUD API با استفاده از فناوریهای زیر ساختیم:
- Next.js
- TypeScript
- Tailwind CSS
- tRPC
- پریسما
- Postgres
- داکر
اگر نسخه ویدیویی را ترجیح می دهید:
https://www.youtube.com/watch?v=Gf9RkaHnsR8
همه کدها به صورت رایگان در GitHub در دسترس هستند (لینک در توضیحات ویدیو).
در اینجا می توانید با فرانچسکو ارتباط برقرار کنید