بناء تطبيق متكامل باستخدام Prisma ORM و Next.js 15 App Router

أشهر ORM لـ TypeScript يلتقي مع حزمة React الحديثة. يمنحك Prisma مخططًا تصريحيًا وأنواعًا مُنشأة تلقائيًا ومحرك استعلامات قويًا — كل ذلك يتكامل بسلاسة مع Next.js 15 Server Components و Server Actions. في هذا الدليل ستبني تطبيقًا كاملاً لإدارة المشاريع من الصفر.
ما ستتعلمه
بنهاية هذا الدليل، ستكون قادرًا على:
- إعداد Prisma ORM مع PostgreSQL في مشروع Next.js 15 App Router
- تعريف النماذج والعلاقات باستخدام Prisma Schema Language
- تشغيل الترحيلات وبذر قاعدة البيانات
- بناء عمليات CRUD كاملة باستخدام Next.js Server Actions
- استخدام Prisma Client لاستعلامات آمنة الأنواع مع التصفية والترقيم
- معالجة التحقق من النماذج باستخدام Zod و
useActionState - النشر في بيئة الإنتاج مع أفضل الممارسات
المتطلبات الأساسية
قبل البدء، تأكد من وجود:
- Node.js 20+ مثبت (
node --version) - خبرة في TypeScript (الأنواع، الواجهات، async/await)
- معرفة بـ Next.js (App Router، Server Components)
- PostgreSQL يعمل محليًا أو مثيل سحابي (سنستخدم Neon)
- محرر أكواد — يُنصح بـ VS Code أو Cursor
لماذا Prisma ORM؟
Prisma هو أكثر ORM انتشارًا لـ TypeScript، تستخدمه شركات مثل Netflix و Notion و Hashicorp. إليك ما يميزه:
| الميزة | Prisma | Drizzle | TypeORM |
|---|---|---|---|
| المخطط | DSL تصريحي (.prisma) | TypeScript | Decorators |
| أمان الأنواع | أنواع مُنشأة تلقائيًا | أنواع مُستنتجة | جزئي |
| الترحيلات | CLI مدمج | drizzle-kit | CLI أو يدوي |
| العلاقات | من الدرجة الأولى، كتابة متداخلة | ربط يدوي | Decorators |
| واجهة الاستعلام | واجهة كائنية بديهية | واجهة شبيهة بـ SQL | نمط Repository |
| Studio | واجهة رسومية مدمجة (Prisma Studio) | Drizzle Studio | لا يوجد |
| دعم Edge | Prisma Accelerate | أصلي | غير محسّن |
يتبع Prisma نهج المخطط أولاً: تعلن نموذج البيانات في ملف .prisma، ويُنشئ Prisma عميلاً مكتملًا مع إكمال تلقائي لكل حقل وعلاقة ومرشح. لا حاجة لتعريف أنواع يدويًا.
ما ستبنيه
تطبيق لإدارة المشاريع يتضمن:
- مشاريع تحتوي على مهام متعددة
- مهام بحالة وأولوية وتواريخ استحقاق
- عمليات CRUD كاملة للمشاريع والمهام
- تصفية وترتيب
- واجهة متجاوبة مع Tailwind CSS
الخطوة 1: إنشاء مشروع Next.js 15
أنشئ مشروعًا جديدًا:
npx create-next-app@latest project-manager --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd project-managerاقبل الإعدادات الافتراضية. هذا ينشئ مشروع Next.js 15 مع App Router و TypeScript و Tailwind CSS.
الخطوة 2: تثبيت Prisma
ثبّت Prisma كاعتماد تطوير و Prisma Client:
npm install prisma --save-dev
npm install @prisma/clientهيّئ Prisma مع PostgreSQL كمزود:
npx prisma init --datasource-provider postgresqlينشئ هذا ملفين:
prisma/schema.prisma— تعريف نموذج البيانات.env— سلسلة اتصال قاعدة البيانات
الخطوة 3: تكوين قاعدة البيانات
افتح .env وعيّن سلسلة اتصال PostgreSQL:
DATABASE_URL="postgresql://user:password@localhost:5432/project_manager?schema=public"إذا كنت تستخدم Neon (يُنصح به للإعداد السريع):
- أنشئ حسابًا مجانيًا على neon.tech
- أنشئ مشروعًا جديدًا
- انسخ سلسلة الاتصال من لوحة التحكم
- الصقها في
.env
إذا كنت تشغّل PostgreSQL محليًا بـ Docker:
docker run --name pg-prisma -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=project_manager -p 5432:5432 -d postgres:16الخطوة 4: تعريف مخطط Prisma
افتح prisma/schema.prisma وعرّف نموذج البيانات:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Project {
id String @id @default(cuid())
name String
description String?
color String @default("#3b82f6")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tasks Task[]
@@map("projects")
}
model Task {
id String @id @default(cuid())
title String
description String?
status TaskStatus @default(TODO)
priority Priority @default(MEDIUM)
dueDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@index([projectId])
@@index([status])
@@map("tasks")
}
enum TaskStatus {
TODO
IN_PROGRESS
DONE
}
enum Priority {
LOW
MEDIUM
HIGH
URGENT
}النقاط الرئيسية في هذا المخطط:
@id @default(cuid())يُنشئ معرّفات فريدة تلقائيًا@updatedAtيُحدّث الطابع الزمني تلقائيًا عند كل تغييرTask[]على Project يُعرّف علاقة واحد-للعديدonDelete: Cascadeيحذف جميع المهام عند حذف المشروع@@indexيُنشئ فهارس قاعدة البيانات لاستعلامات أسرع@@mapيربط النموذج باسم جدول محدد- Enums توفر قيم حالة وأولوية آمنة الأنواع
الخطوة 5: تشغيل أول ترحيل
أنشئ وطبّق الترحيل:
npx prisma migrate dev --name initيقوم هذا الأمر بثلاثة أشياء:
- ينشئ ملف ترحيل SQL في
prisma/migrations/ - يطبّق الترحيل على قاعدة البيانات
- يُنشئ Prisma Client مع أنواع TypeScript كاملة
يمكنك فحص SQL المُنشأ في prisma/migrations/[timestamp]_init/migration.sql.
الخطوة 6: إنشاء Prisma Client Singleton
في بيئة Next.js، يمكن أن يُنشئ إعادة التحميل الساخن عدة مثيلات من Prisma Client. أنشئ singleton لمنع ذلك.
أنشئ src/lib/prisma.ts:
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}هذا يضمن وجود مثيل واحد فقط من PrismaClient أثناء التطوير.
الخطوة 7: بذر قاعدة البيانات
أنشئ prisma/seed.ts لملء قاعدة البيانات ببيانات تجريبية:
import { PrismaClient, TaskStatus, Priority } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
// تنظيف البيانات الحالية
await prisma.task.deleteMany();
await prisma.project.deleteMany();
// إنشاء مشاريع مع مهام
const webApp = await prisma.project.create({
data: {
name: "إعادة تصميم تطبيق الويب",
description: "تجديد شامل لموقع الشركة",
color: "#3b82f6",
tasks: {
create: [
{
title: "تصميم صفحة هبوط جديدة",
description: "إنشاء إطارات سلكية ونماذج للصفحة الرئيسية",
status: TaskStatus.DONE,
priority: Priority.HIGH,
dueDate: new Date("2026-04-15"),
},
{
title: "تنفيذ المصادقة",
description: "إعداد تسجيل الدخول والتسجيل وإعادة تعيين كلمة المرور",
status: TaskStatus.IN_PROGRESS,
priority: Priority.URGENT,
dueDate: new Date("2026-04-20"),
},
{
title: "بناء لوحة التحكم",
description: "إنشاء لوحة تحكم المستخدم الرئيسية مع الرسوم البيانية",
status: TaskStatus.TODO,
priority: Priority.MEDIUM,
dueDate: new Date("2026-05-01"),
},
{
title: "كتابة وثائق API",
status: TaskStatus.TODO,
priority: Priority.LOW,
},
],
},
},
});
const mobileApp = await prisma.project.create({
data: {
name: "النموذج الأولي لتطبيق الجوال",
description: "تطبيق React Native لـ iOS و Android",
color: "#10b981",
tasks: {
create: [
{
title: "إعداد مشروع Expo",
status: TaskStatus.DONE,
priority: Priority.HIGH,
},
{
title: "بناء هيكل التنقل",
status: TaskStatus.IN_PROGRESS,
priority: Priority.HIGH,
dueDate: new Date("2026-04-10"),
},
{
title: "دمج الإشعارات الفورية",
status: TaskStatus.TODO,
priority: Priority.MEDIUM,
dueDate: new Date("2026-05-15"),
},
],
},
},
});
console.log("تم بذر المشاريع:", { webApp: webApp.id, mobileApp: mobileApp.id });
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});أضف أمر البذر إلى package.json:
{
"prisma": {
"seed": "npx tsx prisma/seed.ts"
}
}شغّل البذر:
npx prisma db seedيمكنك التحقق من البيانات باستخدام Prisma Studio:
npx prisma studioيفتح هذا متصفح قاعدة بيانات مرئي على http://localhost:5555.
الخطوة 8: بناء تعريفات الأنواع والتحقق
أنشئ src/lib/validations.ts لمخططات Zod المشتركة:
import { z } from "zod";
export const createProjectSchema = z.object({
name: z.string().min(1, "الاسم مطلوب").max(100),
description: z.string().max(500).optional(),
color: z.string().regex(/^#[0-9a-fA-F]{6}$/, "صيغة لون غير صالحة"),
});
export const createTaskSchema = z.object({
title: z.string().min(1, "العنوان مطلوب").max(200),
description: z.string().max(1000).optional(),
status: z.enum(["TODO", "IN_PROGRESS", "DONE"]).default("TODO"),
priority: z.enum(["LOW", "MEDIUM", "HIGH", "URGENT"]).default("MEDIUM"),
dueDate: z.string().optional(),
projectId: z.string().min(1, "المشروع مطلوب"),
});
export const updateTaskSchema = createTaskSchema.partial().extend({
id: z.string().min(1),
});
export type CreateProjectInput = z.infer<typeof createProjectSchema>;
export type CreateTaskInput = z.infer<typeof createTaskSchema>;
export type UpdateTaskInput = z.infer<typeof updateTaskSchema>;الخطوة 9: إنشاء Server Actions
Server Actions هي الطريقة الموصى بها للتعامل مع التغييرات في Next.js App Router. أنشئ src/app/actions.ts:
"use server";
import { revalidatePath } from "next/cache";
import { prisma } from "@/lib/prisma";
import {
createProjectSchema,
createTaskSchema,
updateTaskSchema,
} from "@/lib/validations";
// ─── إجراءات المشاريع ─────────────────────────────────
export async function createProject(formData: FormData) {
const raw = {
name: formData.get("name") as string,
description: formData.get("description") as string,
color: formData.get("color") as string,
};
const validated = createProjectSchema.safeParse(raw);
if (!validated.success) {
return { error: validated.error.flatten().fieldErrors };
}
await prisma.project.create({
data: validated.data,
});
revalidatePath("/");
return { success: true };
}
export async function deleteProject(id: string) {
await prisma.project.delete({
where: { id },
});
revalidatePath("/");
return { success: true };
}
// ─── إجراءات المهام ─────────────────────────────────────
export async function createTask(formData: FormData) {
const raw = {
title: formData.get("title") as string,
description: formData.get("description") as string,
status: formData.get("status") as string,
priority: formData.get("priority") as string,
dueDate: formData.get("dueDate") as string,
projectId: formData.get("projectId") as string,
};
const validated = createTaskSchema.safeParse(raw);
if (!validated.success) {
return { error: validated.error.flatten().fieldErrors };
}
const { dueDate, ...rest } = validated.data;
await prisma.task.create({
data: {
...rest,
dueDate: dueDate ? new Date(dueDate) : null,
},
});
revalidatePath("/");
return { success: true };
}
export async function updateTaskStatus(id: string, status: string) {
const validated = updateTaskSchema.safeParse({ id, status });
if (!validated.success) {
return { error: "مدخل غير صالح" };
}
await prisma.task.update({
where: { id },
data: { status: validated.data.status },
});
revalidatePath("/");
return { success: true };
}
export async function deleteTask(id: string) {
await prisma.task.delete({
where: { id },
});
revalidatePath("/");
return { success: true };
}الأنماط الرئيسية هنا:
"use server"يحدد الملف كمحتوٍ على Server Actions- التحقق بـ Zod يضمن سلامة البيانات قبل أي عملية على قاعدة البيانات
revalidatePath("/")يمسح الذاكرة المؤقتة لتحديث الواجهة فورًا- قيم إرجاع مُحددة الأنواع تتيح للعميل معالجة حالات النجاح والخطأ
الخطوة 10: بناء صفحة قائمة المشاريع
أنشئ الصفحة الرئيسية في src/app/page.tsx:
import Link from "next/link";
import { prisma } from "@/lib/prisma";
import { deleteProject } from "./actions";
export default async function HomePage() {
const projects = await prisma.project.findMany({
include: {
_count: {
select: { tasks: true },
},
tasks: {
select: { status: true },
},
},
orderBy: { createdAt: "desc" },
});
return (
<main className="max-w-4xl mx-auto p-8">
<div className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-bold">المشاريع</h1>
<Link
href="/projects/new"
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
>
مشروع جديد
</Link>
</div>
<div className="grid gap-4">
{projects.map((project) => {
const done = project.tasks.filter((t) => t.status === "DONE").length;
const total = project._count.tasks;
const progress = total > 0 ? Math.round((done / total) * 100) : 0;
return (
<Link
key={project.id}
href={`/projects/${project.id}`}
className="block border rounded-lg p-6 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div
className="w-4 h-4 rounded-full"
style={{ backgroundColor: project.color }}
/>
<div>
<h2 className="text-xl font-semibold">{project.name}</h2>
{project.description && (
<p className="text-gray-500 mt-1">
{project.description}
</p>
)}
</div>
</div>
<span className="text-sm text-gray-400">
{total} مهمة
</span>
</div>
{total > 0 && (
<div className="mt-4">
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-500">التقدم</span>
<span className="font-medium">{progress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="h-2 rounded-full transition-all"
style={{
width: `${progress}%`,
backgroundColor: project.color,
}}
/>
</div>
</div>
)}
</Link>
);
})}
</div>
{projects.length === 0 && (
<div className="text-center py-12 text-gray-500">
<p className="text-lg">لا توجد مشاريع بعد</p>
<p className="mt-2">أنشئ مشروعك الأول للبدء.</p>
</div>
)}
</main>
);
}لاحظ كيف تعمل استعلامات Prisma مباشرة داخل Server Component — لا حاجة لمسارات API. خيار include يتيح لك جلب البيانات المرتبطة والتجميعات في استعلام واحد.
الخطوة 11: بناء صفحة تفاصيل المشروع
أنشئ src/app/projects/[id]/page.tsx:
import { notFound } from "next/navigation";
import { prisma } from "@/lib/prisma";
import { TaskList } from "./task-list";
import { TaskForm } from "./task-form";
interface Props {
params: Promise<{ id: string }>;
}
export default async function ProjectPage({ params }: Props) {
const { id } = await params;
const project = await prisma.project.findUnique({
where: { id },
include: {
tasks: {
orderBy: [
{ status: "asc" },
{ priority: "desc" },
{ createdAt: "desc" },
],
},
},
});
if (!project) {
notFound();
}
const tasksByStatus = {
TODO: project.tasks.filter((t) => t.status === "TODO"),
IN_PROGRESS: project.tasks.filter((t) => t.status === "IN_PROGRESS"),
DONE: project.tasks.filter((t) => t.status === "DONE"),
};
return (
<main className="max-w-5xl mx-auto p-8">
<div className="mb-8">
<div className="flex items-center gap-3 mb-2">
<div
className="w-5 h-5 rounded-full"
style={{ backgroundColor: project.color }}
/>
<h1 className="text-3xl font-bold">{project.name}</h1>
</div>
{project.description && (
<p className="text-gray-500 text-lg">{project.description}</p>
)}
</div>
<TaskForm projectId={project.id} />
<div className="grid md:grid-cols-3 gap-6 mt-8">
<TaskColumn title="قيد الانتظار" tasks={tasksByStatus.TODO} color="#6b7280" />
<TaskColumn
title="قيد التنفيذ"
tasks={tasksByStatus.IN_PROGRESS}
color="#f59e0b"
/>
<TaskColumn title="مكتمل" tasks={tasksByStatus.DONE} color="#10b981" />
</div>
</main>
);
}
function TaskColumn({
title,
tasks,
color,
}: {
title: string;
tasks: any[];
color: string;
}) {
return (
<div>
<div className="flex items-center gap-2 mb-4">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: color }}
/>
<h2 className="font-semibold text-lg">{title}</h2>
<span className="text-sm text-gray-400 ml-auto">{tasks.length}</span>
</div>
<TaskList tasks={tasks} />
</div>
);
}الخطوة 12: بناء مكونات المهام
أنشئ مكون قائمة المهام في src/app/projects/[id]/task-list.tsx:
"use client";
import { updateTaskStatus, deleteTask } from "@/app/actions";
import { useTransition } from "react";
interface Task {
id: string;
title: string;
description: string | null;
status: string;
priority: string;
dueDate: string | null;
}
const priorityColors: Record<string, string> = {
LOW: "bg-gray-100 text-gray-600",
MEDIUM: "bg-blue-100 text-blue-600",
HIGH: "bg-orange-100 text-orange-600",
URGENT: "bg-red-100 text-red-600",
};
export function TaskList({ tasks }: { tasks: Task[] }) {
return (
<div className="space-y-3">
{tasks.map((task) => (
<TaskCard key={task.id} task={task} />
))}
{tasks.length === 0 && (
<p className="text-gray-400 text-sm text-center py-4">لا توجد مهام</p>
)}
</div>
);
}
function TaskCard({ task }: { task: Task }) {
const [isPending, startTransition] = useTransition();
const handleStatusChange = (newStatus: string) => {
startTransition(() => {
updateTaskStatus(task.id, newStatus);
});
};
const handleDelete = () => {
startTransition(() => {
deleteTask(task.id);
});
};
return (
<div
className={`border rounded-lg p-4 bg-white ${
isPending ? "opacity-50" : ""
}`}
>
<div className="flex items-start justify-between">
<h3 className="font-medium">{task.title}</h3>
<button
onClick={handleDelete}
className="text-gray-400 hover:text-red-500 text-sm"
>
×
</button>
</div>
{task.description && (
<p className="text-gray-500 text-sm mt-1">{task.description}</p>
)}
<div className="flex items-center gap-2 mt-3">
<span
className={`text-xs px-2 py-1 rounded-full ${
priorityColors[task.priority]
}`}
>
{task.priority}
</span>
{task.dueDate && (
<span className="text-xs text-gray-400">
الاستحقاق {new Date(task.dueDate).toLocaleDateString("ar")}
</span>
)}
</div>
<div className="flex gap-1 mt-3">
{["TODO", "IN_PROGRESS", "DONE"].map((status) => (
<button
key={status}
onClick={() => handleStatusChange(status)}
disabled={task.status === status}
className={`text-xs px-2 py-1 rounded ${
task.status === status
? "bg-gray-900 text-white"
: "bg-gray-100 hover:bg-gray-200 text-gray-600"
}`}
>
{status.replace("_", " ")}
</button>
))}
</div>
</div>
);
}أنشئ نموذج المهام في src/app/projects/[id]/task-form.tsx:
"use client";
import { createTask } from "@/app/actions";
import { useRef } from "react";
import { useActionState } from "react";
const initialState = { error: null as any, success: false };
export function TaskForm({ projectId }: { projectId: string }) {
const formRef = useRef<HTMLFormElement>(null);
async function action(_prev: typeof initialState, formData: FormData) {
formData.set("projectId", projectId);
const result = await createTask(formData);
if (result.success) {
formRef.current?.reset();
}
return result as typeof initialState;
}
const [state, formAction, isPending] = useActionState(action, initialState);
return (
<form ref={formRef} action={formAction} className="border rounded-lg p-4">
<h3 className="font-semibold mb-3">إضافة مهمة جديدة</h3>
<div className="grid md:grid-cols-2 gap-3">
<input
name="title"
placeholder="عنوان المهمة"
required
className="border rounded px-3 py-2"
/>
<select name="priority" className="border rounded px-3 py-2">
<option value="LOW">أولوية منخفضة</option>
<option value="MEDIUM" selected>أولوية متوسطة</option>
<option value="HIGH">أولوية عالية</option>
<option value="URGENT">عاجل</option>
</select>
<input
name="description"
placeholder="الوصف (اختياري)"
className="border rounded px-3 py-2"
/>
<input
name="dueDate"
type="date"
className="border rounded px-3 py-2"
/>
</div>
<input type="hidden" name="status" value="TODO" />
{state.error && (
<p className="text-red-500 text-sm mt-2">
{Object.values(state.error).flat().join("، ")}
</p>
)}
<button
type="submit"
disabled={isPending}
className="mt-3 bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
{isPending ? "جاري الإضافة..." : "إضافة مهمة"}
</button>
</form>
);
}الخطوة 13: استعلامات Prisma المتقدمة
يتفوق Prisma في الاستعلامات المعقدة. إليك أنماطًا ستستخدمها بشكل متكرر:
التصفية والترقيم
// جلب المهام مع مرشحات
const tasks = await prisma.task.findMany({
where: {
projectId: "some-id",
status: "IN_PROGRESS",
priority: { in: ["HIGH", "URGENT"] },
dueDate: { lte: new Date() }, // المهام المتأخرة
},
orderBy: { priority: "desc" },
skip: 0,
take: 20,
});التجميعات
// عدّ المهام حسب الحالة لكل مشروع
const stats = await prisma.task.groupBy({
by: ["projectId", "status"],
_count: { id: true },
});الكتابة المتداخلة (المعاملات)
// إنشاء مشروع مع مهام في معاملة واحدة
const project = await prisma.project.create({
data: {
name: "مشروع جديد",
tasks: {
createMany: {
data: [
{ title: "المهمة 1", priority: "HIGH" },
{ title: "المهمة 2", priority: "MEDIUM" },
],
},
},
},
include: { tasks: true },
});المعاملات التفاعلية
// نقل جميع المهام من مشروع إلى آخر
await prisma.$transaction(async (tx) => {
const tasks = await tx.task.findMany({
where: { projectId: sourceId },
});
await tx.task.updateMany({
where: { projectId: sourceId },
data: { projectId: targetId },
});
await tx.project.delete({
where: { id: sourceId },
});
return tasks.length;
});الخطوة 14: إضافة البحث والتصفية
أنشئ مكون البحث في src/app/search.tsx:
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback } from "react";
export function SearchBar() {
const router = useRouter();
const searchParams = useSearchParams();
const updateFilter = useCallback(
(key: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set(key, value);
} else {
params.delete(key);
}
router.push(`/?${params.toString()}`);
},
[router, searchParams]
);
return (
<div className="flex gap-3 mb-6">
<input
type="search"
placeholder="البحث في المشاريع..."
defaultValue={searchParams.get("q") ?? ""}
onChange={(e) => updateFilter("q", e.target.value)}
className="border rounded px-3 py-2 flex-1"
/>
<select
defaultValue={searchParams.get("sort") ?? "newest"}
onChange={(e) => updateFilter("sort", e.target.value)}
className="border rounded px-3 py-2"
>
<option value="newest">الأحدث أولاً</option>
<option value="oldest">الأقدم أولاً</option>
<option value="name">حسب الاسم</option>
</select>
</div>
);
}حدّث الصفحة الرئيسية لاستخدام معاملات البحث:
interface Props {
searchParams: Promise<{ q?: string; sort?: string }>;
}
export default async function HomePage({ searchParams }: Props) {
const { q, sort } = await searchParams;
const projects = await prisma.project.findMany({
where: q
? {
OR: [
{ name: { contains: q, mode: "insensitive" } },
{ description: { contains: q, mode: "insensitive" } },
],
}
: undefined,
include: {
_count: { select: { tasks: true } },
tasks: { select: { status: true } },
},
orderBy:
sort === "name"
? { name: "asc" }
: sort === "oldest"
? { createdAt: "asc" }
: { createdAt: "desc" },
});
// ... بقية المكون
}الخطوة 15: أفضل ممارسات الإنتاج
تجميع الاتصالات
لبيئات serverless، كوّن تجميع الاتصالات في المخطط:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}عيّن DATABASE_URL لسلسلة اتصال مُجمّعة (PgBouncer أو Neon pooler أو Prisma Accelerate) و DIRECT_URL للاتصال المباشر (يُستخدم فقط للترحيلات).
Prisma Accelerate
للنشر على الحافة، فعّل Prisma Accelerate:
npm install @prisma/extension-accelerateحدّث العميل:
import { PrismaClient } from "@prisma/client";
import { withAccelerate } from "@prisma/extension-accelerate";
export const prisma = new PrismaClient().$extends(withAccelerate());هذا يُمكّن التخزين المؤقت العالمي وتجميع الاتصالات على الحافة.
التسجيل
فعّل تسجيل الاستعلامات في التطوير:
export const prisma = new PrismaClient({
log:
process.env.NODE_ENV === "development"
? ["query", "error", "warn"]
: ["error"],
});استكشاف الأخطاء وإصلاحها
"PrismaClientInitializationError: Can't reach database server"
- تحقق من
DATABASE_URLفي.env - تأكد أن PostgreSQL يعمل
- تحقق من الوصول للشبكة (جدار الحماية، VPN)
الأنواع لا تتحدث بعد تغييرات المخطط
شغّل إنشاء العميل يدويًا:
npx prisma generateهذا يُعيد إنشاء أنواع TypeScript من المخطط.
"Unique constraint violation"
بياناتك تحتوي على قيم مكررة في حقل فريد. إما حدّث السجل المتعارض أو استخدم upsert:
await prisma.project.upsert({
where: { id: "existing-id" },
update: { name: "اسم محدّث" },
create: { name: "مشروع جديد" },
});انحراف الترحيل في الإنتاج
إذا انحرف مخطط قاعدة البيانات عن الترحيلات:
npx prisma migrate resolve --applied "migration_name"أو إعادة تعيين (التطوير فقط):
npx prisma migrate resetالخطوات التالية
الآن بعد أن لديك تطبيق متكامل يعمل مع Prisma و Next.js، استكشف هذه المواضيع:
- إضافة المصادقة مع NextAuth.js أو Better Auth لتحديد المشاريع لكل مستخدم
- تنفيذ التحديثات الفورية مع Prisma Pulse للوحات المهام الحية
- إضافة رفع الملفات لمرفقات المهام باستخدام Vercel Blob أو S3
- إعداد CI/CD مع GitHub Actions لتشغيل الترحيلات تلقائيًا
- استكشاف إضافات Prisma Client للـ middleware والحذف الناعم وتسجيل المراجعة
الخلاصة
في هذا الدليل، بنيت تطبيقًا كاملاً لإدارة المشاريع باستخدام Prisma ORM و Next.js 15 App Router. تعلمت كيف:
- تعريف نموذج بيانات بـ Prisma Schema Language مع العلاقات والتعدادات
- تشغيل الترحيلات وبذر قاعدة البيانات
- إنشاء Prisma Client singleton لـ Next.js
- بناء Server Actions آمنة الأنواع لجميع عمليات CRUD
- استخدام واجهة استعلامات Prisma للتصفية والترقيم والتجميعات
- التعامل مع الكتابة المتداخلة والمعاملات للعمليات المعقدة
- تنفيذ البحث والتصفية مع معاملات بحث URL
- تكوين نشر الإنتاج مع تجميع الاتصالات ودعم الحافة
يزيل مخطط Prisma التصريحي والأنواع المُنشأة تلقائيًا فئة كاملة من الأخطاء — لا يمكنك الاستعلام عن حقل غير موجود أو تمرير نوع خاطئ أو نسيان علاقة مطلوبة. مع Next.js Server Components و Server Actions، تحصل على تجربة تطوير متكاملة آمنة الأنواع من قاعدة البيانات إلى واجهة المستخدم.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

البحث النصي الكامل في PostgreSQL مع Next.js — بناء بحث قوي بدون Elasticsearch (2026)
تعلّم كيفية بناء بحث نصي كامل سريع ومتسامح مع الأخطاء الإملائية باستخدام إمكانيات PostgreSQL المدمجة مع Next.js App Router. لا حاجة لـ Elasticsearch أو Algolia — فقط قاعدة بيانات Postgres الموجودة لديك.

بناء تطبيق متكامل باستخدام Drizzle ORM و Next.js 15: قاعدة بيانات آمنة الأنواع من الصفر إلى الإنتاج
تعلّم كيفية بناء تطبيق متكامل آمن الأنواع باستخدام Drizzle ORM مع Next.js 15. يغطي هذا الدليل العملي تصميم المخططات والترحيلات وServer Actions وعمليات CRUD والنشر مع PostgreSQL.

Neon Serverless Postgres مع Next.js App Router: بناء تطبيق كامل مع تفريع قواعد البيانات
تعلّم كيفية بناء تطبيق Next.js كامل مدعوم بقاعدة بيانات Neon Serverless Postgres. يغطي هذا الدليل التطبيقي برنامج التشغيل بدون خادم، تفريع قواعد البيانات لعمليات النشر التجريبية، تجميع الاتصالات، وأنماط الإنتاج الجاهزة.