بناء واجهة برمجية احترافية باستخدام tRPC و Prisma و Next.js

AI Bot
بواسطة AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

بناء واجهة برمجية احترافية باستخدام tRPC و Prisma و Next.js

بناء الواجهات البرمجية الحديثة يتطلب التوازن بين تجربة المطور، أمان الأنواع (Type Safety)، والأداء. الواجهات البرمجية التقليدية من نوع REST تفتقر إلى أمان الأنواع من الطرف إلى الطرف، بينما GraphQL يضيف تعقيدًا كبيرًا. tRPC يقدم حلاً أنيقًا: أمان كامل للأنواع في TypeScript بدون توليد أكواد، مع Prisma لإدارة قواعد البيانات و Next.js 15 للعرض من الخادم والعميل.

في نهاية هذا الدليل، ستكون قد بنيت واجهة برمجية كاملة لإدارة المهام مع المصادقة، عمليات قاعدة البيانات، وعميل React—كل ذلك مع أمان كامل للأنواع من قاعدة البيانات إلى الواجهة.

لماذا tRPC + Prisma؟

tRPC (TypeScript Remote Procedure Call) يلغي مشكلة عقد الواجهة البرمجية. عندما تغير الواجهة الخلفية، تتحدث أنواع الواجهة الأمامية تلقائيًا. لا توثيق يدوي للواجهة البرمجية، لا مخططات OpenAPI، ولا خطوات توليد أكواد.

Prisma يوفر ORM آمن من حيث الأنواع مع الترحيلات (Migrations)، الاستبطان، وتجربة مطور ممتازة. بالجمع مع tRPC، تحصل على أمان الأنواع من مخطط قاعدة البيانات إلى مكونات React.

الفوائد الحقيقية:

  • اكتشاف الأخطاء في وقت الترجمة، ليس وقت التشغيل
  • إعادة هيكلة بثقة (غيّر اسم حقل، TypeScript يجد جميع الاستخدامات)
  • الإكمال التلقائي في كل مكان
  • تقليل الشيفرة الإضافية مقارنة بـ REST أو GraphQL

المتطلبات الأساسية

قبل البدء، تأكد من توفر:

  • Node.js 18+ (تحقق: `node -v`)
  • npm أو yarn أو pnpm (سنستخدم pnpm)
  • PostgreSQL مثبت محليًا أو قاعدة بيانات سحابية
  • معرفة أساسية بـ TypeScript و React
  • إلمام بأساسيات Next.js

تثبيت PostgreSQL:

# macOS
brew install postgresql@15
brew services start postgresql@15
 
# Ubuntu/Debian
sudo apt update && sudo apt install postgresql postgresql-contrib
sudo systemctl start postgresql

الخطوة 1: إعداد المشروع

إنشاء مشروع Next.js جديد:

pnpm create next-app@latest trpc-task-api --typescript --tailwind --app --eslint
cd trpc-task-api

تثبيت الاعتماديات:

pnpm add @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query@^5
pnpm add @prisma/client zod superjson
pnpm add -D prisma

تهيئة Prisma:

pnpx prisma init

الخطوة 2: مخطط قاعدة البيانات

تعديل `prisma/schema.prisma`:

generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
  tasks     Task[]
}
 
model Task {
  id          String     @id @default(cuid())
  title       String
  description String?
  status      TaskStatus @default(TODO)
  priority    Priority   @default(MEDIUM)
  userId      String
  user        User       @relation(fields: [userId], references: [id], onDelete: Cascade)
  createdAt   DateTime   @default(now())
  updatedAt   DateTime   @updatedAt
  @@index([userId])
}
 
enum TaskStatus {
  TODO
  IN_PROGRESS
  DONE
}
 
enum Priority {
  LOW
  MEDIUM
  HIGH
  URGENT
}

تحديث `.env`:

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

تنفيذ الترحيل:

pnpx prisma migrate dev --name init

الخطوة 3: عميل Prisma

إنشاء `lib/prisma.ts`:

import { PrismaClient } from '@prisma/client'
 
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }
 
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
  log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
})
 
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

لماذا هذا النمط؟ في Next.js، إعادة التحميل الساخن تنشئ عدة نسخ من Prisma. هذا النمط يمنع خطأ "عدد كبير جدًا من الاتصالات".

الخطوة 4: إعداد tRPC

إنشاء `lib/trpc/trpc.ts`:

import { initTRPC, TRPCError } from '@trpc/server'
import { prisma } from '@/lib/prisma'
import superjson from 'superjson'
 
export const createTRPCContext = async (opts: { headers: Headers }) => {
  return {
    prisma,
    userId: opts.headers.get('x-user-id') || undefined,
  }
}
 
const t = initTRPC.context<typeof createTRPCContext>().create({
  transformer: superjson,
})
 
export const router = t.router
export const publicProcedure = t.procedure
 
export const protectedProcedure = t.procedure.use(async (opts) => {
  if (!opts.ctx.userId) {
    throw new TRPCError({ code: 'UNAUTHORIZED' })
  }
  return opts.next({ ctx: { ...opts.ctx, userId: opts.ctx.userId } })
})

المفاهيم الأساسية:

  • Context – البيانات المتاحة لجميع الإجراءات (قاعدة البيانات، جلسة المستخدم، إلخ)
  • Transformer – `superjson` يسمح بكائنات Date وأنواع غير JSON
  • Procedures – مثل الدوال المعروضة عبر الواجهة البرمجية
  • Middleware – `protectedProcedure` يتحقق من المصادقة

الخطوة 5: موجه المهام

إنشاء `lib/trpc/routers/task.ts`:

import { z } from 'zod'
import { router, protectedProcedure } from '../trpc'
import { TaskStatus, Priority } from '@prisma/client'
import { TRPCError } from '@trpc/server'
 
export const taskRouter = router({
  list: protectedProcedure
    .input(z.object({ 
      status: z.nativeEnum(TaskStatus).optional(),
      limit: z.number().min(1).max(100).default(50) 
    }))
    .query(async ({ ctx, input }) => {
      return await ctx.prisma.task.findMany({
        where: { 
          userId: ctx.userId,
          ...(input.status && { status: input.status })
        },
        orderBy: [{ priority: 'desc' }, { createdAt: 'desc' }],
        take: input.limit,
      })
    }),
 
  getById: protectedProcedure
    .input(z.object({ id: z.string().cuid() }))
    .query(async ({ ctx, input }) => {
      const task = await ctx.prisma.task.findUnique({ where: { id: input.id } })
      if (!task || task.userId !== ctx.userId) {
        throw new TRPCError({ code: 'NOT_FOUND' })
      }
      return task
    }),
 
  create: protectedProcedure
    .input(z.object({
      title: z.string().min(1).max(200),
      description: z.string().max(2000).optional(),
      priority: z.nativeEnum(Priority).default('MEDIUM'),
    }))
    .mutation(async ({ ctx, input }) => {
      return await ctx.prisma.task.create({
        data: { ...input, userId: ctx.userId },
      })
    }),
 
  update: protectedProcedure
    .input(z.object({
      id: z.string().cuid(),
      title: z.string().min(1).max(200).optional(),
      description: z.string().max(2000).optional().nullable(),
      status: z.nativeEnum(TaskStatus).optional(),
      priority: z.nativeEnum(Priority).optional(),
    }))
    .mutation(async ({ ctx, input }) => {
      const { id, ...data } = input
      const existing = await ctx.prisma.task.findUnique({ where: { id } })
      if (!existing || existing.userId !== ctx.userId) {
        throw new TRPCError({ code: 'NOT_FOUND' })
      }
      return await ctx.prisma.task.update({ where: { id }, data })
    }),
 
  delete: protectedProcedure
    .input(z.object({ id: z.string().cuid() }))
    .mutation(async ({ ctx, input }) => {
      const existing = await ctx.prisma.task.findUnique({ where: { id: input.id } })
      if (!existing || existing.userId !== ctx.userId) {
        throw new TRPCError({ code: 'NOT_FOUND' })
      }
      await ctx.prisma.task.delete({ where: { id: input.id } })
      return { success: true }
    }),
 
  stats: protectedProcedure.query(async ({ ctx }) => {
    const [total, todo, inProgress, done] = await Promise.all([
      ctx.prisma.task.count({ where: { userId: ctx.userId } }),
      ctx.prisma.task.count({ where: { userId: ctx.userId, status: 'TODO' } }),
      ctx.prisma.task.count({ where: { userId: ctx.userId, status: 'IN_PROGRESS' } }),
      ctx.prisma.task.count({ where: { userId: ctx.userId, status: 'DONE' } }),
    ])
    return { total, todo, inProgress, done }
  }),
})

هيكل الموجه:

  • التحقق من المدخلات – مخططات Zod تمنع البيانات غير الصحيحة
  • التفويض – التحقق من ملكية المهمة قبل التعديلات
  • استنتاج الأنواع – TypeScript يعرف أنواع المدخلات/المخرجات بدقة

الخطوة 6: الموجه الجذري

إنشاء `lib/trpc/root.ts`:

import { router } from './trpc'
import { taskRouter } from './routers/task'
 
export const appRouter = router({
  task: taskRouter,
})
 
export type AppRouter = typeof appRouter

الخطوة 7: مسار الواجهة البرمجية

إنشاء `app/api/trpc/[trpc]/route.ts`:

import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import { appRouter } from '@/lib/trpc/root'
import { createTRPCContext } from '@/lib/trpc/trpc'
 
const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: createTRPCContext,
  })
 
export { handler as GET, handler as POST }

الخطوة 8: إعداد العميل

إنشاء `lib/trpc/client.ts`:

import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from './root'
 
export const trpc = createTRPCReact<AppRouter>()

إنشاء `app/providers.tsx`:

'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { httpBatchLink } from '@trpc/client'
import { trpc } from '@/lib/trpc/client'
import { useState } from 'react'
import superjson from 'superjson'
 
export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient())
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: \`\${getBaseUrl()}/api/trpc\`,
          transformer: superjson,
          headers() {
            return { 'x-user-id': 'demo-user-123' }
          },
        }),
      ],
    })
  )
 
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </trpc.Provider>
  )
}
 
function getBaseUrl() {
  if (typeof window !== 'undefined') return ''
  if (process.env.VERCEL_URL) return \`https://\${process.env.VERCEL_URL}\`
  return \`http://localhost:\${process.env.PORT ?? 3000}\`
}

تحديث `app/layout.tsx`:

import { Providers } from './providers'
import './globals.css'
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ar" dir="rtl">
      <body><Providers>{children}</Providers></body>
    </html>
  )
}

الخطوة 9: مكون الواجهة

إنشاء `app/tasks/page.tsx`:

'use client'
import { trpc } from '@/lib/trpc/client'
import { useState } from 'react'
 
export default function TasksPage() {
  const [title, setTitle] = useState('')
  const utils = trpc.useUtils()
  const { data: tasks, isLoading } = trpc.task.list.useQuery({ limit: 50 })
  const { data: stats } = trpc.task.stats.useQuery()
 
  const createTask = trpc.task.create.useMutation({
    onSuccess: () => {
      utils.task.list.invalidate()
      utils.task.stats.invalidate()
      setTitle('')
    },
  })
 
  const updateTask = trpc.task.update.useMutation({
    onSuccess: () => {
      utils.task.list.invalidate()
      utils.task.stats.invalidate()
    },
  })
 
  const deleteTask = trpc.task.delete.useMutation({
    onSuccess: () => {
      utils.task.list.invalidate()
      utils.task.stats.invalidate()
    },
  })
 
  if (isLoading) return <div className="p-8">جاري التحميل...</div>
 
  return (
    <div className="max-w-4xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-6">مدير المهام</h1>
 
      {stats && (
        <div className="grid grid-cols-4 gap-4 mb-8">
          <div className="p-4 border rounded"><div className="text-2xl font-bold">{stats.total}</div><div className="text-sm text-gray-600">الإجمالي</div></div>
          <div className="p-4 border rounded"><div className="text-2xl font-bold">{stats.todo}</div><div className="text-sm text-gray-600">للإنجاز</div></div>
          <div className="p-4 border rounded"><div className="text-2xl font-bold">{stats.inProgress}</div><div className="text-sm text-gray-600">قيد التنفيذ</div></div>
          <div className="p-4 border rounded"><div className="text-2xl font-bold">{stats.done}</div><div className="text-sm text-gray-600">مكتمل</div></div>
        </div>
      )}
 
      <form onSubmit={(e) => { e.preventDefault(); createTask.mutate({ title }) }} className="mb-8">
        <div className="flex gap-2">
          <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} 
            placeholder="مهمة جديدة..." className="flex-1 px-4 py-2 border rounded" />
          <button type="submit" disabled={createTask.isPending} 
            className="px-6 py-2 bg-blue-600 text-white rounded">إضافة</button>
        </div>
      </form>
 
      <div className="space-y-2">
        {tasks?.map((task) => (
          <div key={task.id} className="flex items-center gap-4 p-4 border rounded">
            <input type="checkbox" checked={task.status === 'DONE'} 
              onChange={() => updateTask.mutate({ id: task.id, status: task.status === 'DONE' ? 'TODO' : 'DONE' })} />
            <div className="flex-1">
              <h3 className={\`font-medium \${task.status === 'DONE' ? 'line-through' : ''}\`}>{task.title}</h3>
            </div>
            <span className="px-2 py-1 text-xs rounded bg-gray-100">{task.priority}</span>
            <button onClick={() => deleteTask.mutate({ id: task.id })} className="text-red-600">حذف</button>
          </div>
        ))}
      </div>
    </div>
  )
}

الخطوة 10: تشغيل التطبيق

pnpm dev

زيارة http://localhost:3000/tasks

أفضل ممارسات الإنتاج

1. المصادقة

استبدل الرأس التجريبي بـ NextAuth.js أو Clerk:

import { getServerSession } from 'next-auth'
 
export const createTRPCContext = async () => {
  const session = await getServerSession()
  return { prisma, userId: session?.user?.id }
}

2. تحديد المعدل

استخدم Upstash أو مكتبة مماثلة لمنع الاستخدام الزائد.

3. مراقبة الأخطاء

أضف Sentry أو أداة مماثلة لتتبع الأخطاء في الإنتاج.

4. تجميع الاتصالات

للنشر على Serverless، استخدم Prisma Accelerate أو PgBouncer.

5. التخزين المؤقت

نفّذ التخزين المؤقت باستخدام Redis للبيانات المطلوبة بشكل متكرر.

النشر

Vercel

pnpx vercel

اضبط `DATABASE_URL` في متغيرات البيئة.

Docker

FROM node:20-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install
COPY . .
RUN pnpx prisma generate
RUN pnpm build
EXPOSE 3000
CMD ["pnpm", "start"]

الخلاصة

لقد بنيت واجهة برمجية احترافية مع:

✅ أمان الأنواع من الطرف إلى الطرف من قاعدة البيانات إلى الواجهة ✅ التحقق من المدخلات باستخدام Zod ✅ المصادقة والتفويض ✅ ترحيلات قاعدة البيانات مع Prisma ✅ التخزين المؤقت والتحديثات المتفائلة مع React Query ✅ تجميع الطلبات لتحسين الأداء

النقاط الأساسية

  1. tRPC يلغي عقود الواجهة البرمجية – الأنواع تتزامن تلقائيًا
  2. أنواع Prisma تتدفق إلى tRPC – مخطط قاعدة البيانات يقود أنواع الواجهة البرمجية
  3. Middleware يمكّن الأنماط القابلة لإعادة الاستخدام – المصادقة، التسجيل، التحقق
  4. React Query يشغل العميل – التخزين المؤقت، إعادة المحاولة، التحولات
  5. TypeScript يكتشف الأخطاء مبكرًا – الأمان في وقت الترجمة

الخطوات التالية

  • إضافة اشتراكات WebSocket للتحديثات الفورية
  • تنفيذ رفع الملفات
  • إضافة نقطة نهاية OpenAPI للتكاملات الخارجية
  • بناء تطبيق هاتف محمول باستخدام React Native + tRPC
  • تنفيذ أنماط CQRS مع قواعد بيانات منفصلة

هل تحتاج مساعدة في بناء واجهات برمجية احترافية؟

نقطة متخصصة في بناء واجهات برمجية قوية وآمنة من حيث الأنواع وتطبيقات كاملة:

احجز استشارة مجانية: تواصل مع نقطة


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على محول MCP لووردبريس: اجعل موقعك جاهزا لوكلاء الذكاء الاصطناعي.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة