دليل Full-Stack: Next.js App Router و Server Actions و Prisma

دليل Full-Stack: Next.js App Router و Server Actions و Prisma

عبدالرحمن الجماز
عبدالرحمن الجماز
مؤسس في Tech Core X
12 دقيقة قراءة

دليل متكامل لبناء تطبيق Next.js حديث ومتكامل (full-stack)، من مخطط قاعدة البيانات إلى واجهة المستخدم التفاعلية باستخدام Server Actions.

دليل Full-Stack: Next.js App Router و Server Actions و Prisma

أصبح بناء تطبيق full-stack باستخدام Next.js أكثر سلاسة من أي وقت مضى. من خلال الجمع بين مكونات الخادم (Server Components) وإجراءات الخادم (Server Actions) و ORM مثل Prisma، يمكنك إنشاء تطبيقات عالية الأداء وآمنة مع تجربة مطور رائعة. سيرشدك هذا الدليل خلال العملية بأكملها.

1. الهيكلية (The Architecture)

أولاً، دعنا نفهم تدفق البيانات. في هذا النموذج، تتصل مكونات الخادم (أو إجراءات الخادم) مباشرة بقاعدة البيانات. واجهة المستخدم من جانب العميل مسؤولة فقط عن عرض البيانات واستدعاء الإجراءات.

graph TD
    A[المتصفح (مكون عميل)] -- 1. يستدعي الإجراء --> B[خادم Next.js (إجراء خادم)];
    B -- 2. ينفذ المنطق --> C(Prisma Client);
    C -- 3. يستعلم من --> D[قاعدة البيانات];
    D -- 4. تُرجع البيانات --> C;
    C -- 5. تُرجع البيانات --> B;
    B -- 6. يعيد التحقق من المسار ويُرجع --> A;
    A -- 7. يعيد العرض ببيانات جديدة --> A;

هذا النمط قوي بشكل لا يصدق. لا يصل العميل أبدًا إلى قاعدة البيانات مباشرة، ويبقى كل المنطق الحساس (مثل process.env.DATABASE_URL) على الخادم.

2. إعداد قاعدة البيانات

سنستخدم Prisma، وهو ORM حديث لـ Node.js و TypeScript. يجعل الوصول إلى قاعدة البيانات بسيطًا وآمنًا من حيث النوع (type-safe).

أولاً، قم بتثبيت Prisma CLI:

npm install prisma --save-dev

بعد ذلك، قم بتهيئة Prisma:

npx prisma init --datasource-provider sqlite

سيؤدي هذا إلى إنشاء ملف prisma/schema.prisma. دعنا نعرّف نموذج Post بسيط.

// prisma/schema.prisma

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

generator client {
  provider = "prisma-client-js"
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?
  createdAt DateTime @default(now())
}

أخيرًا، قم بتشغيل الترحيل (migration) لإنشاء ملف قاعدة البيانات الخاص بك:

npx prisma migrate dev --name init

3. إنشاء إجراءات الخادم (Server Actions)

Server Actions هي دوال تعمل على الخادم، يتم تشغيلها من العميل. دعنا ننشئ إجراءً لإضافة منشور جديد.

أنشئ ملفًا جديدًا، على سبيل المثال، app/actions.ts:

// app/actions.ts
'use server'

import { PrismaClient } from '@prisma/client'
import { revalidatePath } from 'next/cache'

const prisma = new PrismaClient()

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  if (!title) {
    return { error: 'العنوان مطلوب' };
  }

  try {
    await prisma.post.create({
      data: {
        title,
        content,
      },
    });

    revalidatePath('/blog'); // يخبر Next.js بإعادة جلب صفحة /blog
    return { success: true };
  } catch (e) {
    return { error: 'فشل إنشاء المنشور' };
  }
}

تحليل الإجراء

  • 'use server': هذا التوجيه يحدد هذا الملف (وجميع صادراته) كإجراءات خادم.
  • prisma.post.create: نستخدم Prisma client لإنشاء سجل جديد بأمان. هذا الكود يعمل فقط على الخادم.
  • revalidatePath('/blog'): هذا هو السحر. يقوم بمسح ذاكرة التخزين المؤقت لصفحة المدونة، مما يجبر مكونات الخادم على إعادة جلب البيانات وإظهار المنشور الجديد.

4. بناء واجهة المستخدم

الآن يمكننا إنشاء مكون عميل (client component) يحتوي على نموذج (form) يستدعي هذا الإجراء.

صفحة الخادم

أولاً، الصفحة نفسها (app/blog/page.tsx) ستكون مكون خادم يجلب جميع المنشورات.

// app/blog/page.tsx
import { PrismaClient } from '@prisma/client'
import { CreatePostForm } from './CreatePostForm'

const prisma = new PrismaClient()

async function getPosts() {
  const posts = await prisma.post.findMany({
    orderBy: { createdAt: 'desc' },
  });
  return posts;
}

export default async function BlogPage() {
  const posts = await getPosts();

  return (
    <div>
      <h1 className="text-4xl font-bold mb-8">المنشورات</h1>
      
      {/* النموذج هو مكون عميل */}
      <CreatePostForm />

      {/* يتم عرض القائمة على الخادم */}
      <div className="mt-8 space-y-4">
        {posts.map((post) => (
          <div key={post.id} className="p-4 border rounded-lg">
            <h3 className="font-semibold">{post.title}</h3>
            <p>{post.content}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

نموذج العميل

هذا المكون (app/blog/CreatePostForm.tsx) يجب أن يكون مكون عميل لأنه يستخدم useRef لمسح النموذج.

// app/blog/CreatePostForm.tsx
'use client'

import { useRef } from 'react'
import { createPost } from '@/app/actions'

export function CreatePostForm() {
  const formRef = useRef<HTMLFormElement>(null);

  const handleAction = async (formData: FormData) => {
    const result = await createPost(formData);
    if (result?.success) {
      formRef.current?.reset(); // مسح النموذج عند النجاح
    } else {
      alert(result?.error);
    }
  };

  return (
    <form ref={formRef} action={handleAction} className="space-y-4">
      <div>
        <label>العنوان</label>
        <input name="title" type="text" className="input" />
      </div>
      <div>
        <label>المحتوى</label>
        <textarea name="content" className="textarea" />
      </div>
      <button type="submit" className="button">إرسال المنشور</button>
    </form>
  );
}

خاتمة

هذا النمط هو مستقبل تطوير React full-stack. من خلال الاستفادة من مكونات الخادم لجلب البيانات وإجراءات الخادم للتعديلات (mutations)، تحصل على تطبيق بسيط، متجاور (co-located)، وآمن للغاية، وهو أيضًا سريع بشكل لا يصدق.

دليل Full-Stack: Next.js App Router و Server Actions و Prisma