ساخت اپلیکیشن فول‌استک با Next.js 15 و Drizzle ORM: راهنمای CRUD با Server Actions

با Next.js 15 و Drizzle ORM یه اپلیکیشن فول‌استک بسازید. از اتصال به Neon PostgreSQL و تعریف اسکیما تا پیاده‌سازی CRUD کامل با Server Actions و اعتبارسنجی Zod — همه چیز با کد عملی و قابل اجرا.

مقدمه: چرا Drizzle ORM بهترین انتخاب برای Next.js 15 هست؟

اگه مقاله‌های قبلی ما رو دنبال کرده باشید، تا الان با احراز هویت Auth.js v5 و واکشی داده و کشینگ در Next.js 15 آشنا شدید. حالا وقتشه بریم سراغ یکی از مهم‌ترین بخش‌های هر اپلیکیشن فول‌استک: اتصال به پایگاه داده و مدیریت عملیات CRUD.

خب، وقتی حرف از ORM برای پروژه‌های Next.js میاد، اکثر توسعه‌دهنده‌ها اول اسم Prisma به ذهنشون میاد. ولی صادقانه بگم، یه رقیب جدی هست که تو سال ۲۰۲۶ داره حسابی بازار رو قبضه می‌کنه: Drizzle ORM.

این ORM با بیش از ۲۵ هزار ستاره در GitHub، حجم فوق‌العاده کم (حدود ۷.۴ کیلوبایت فشرده‌شده!)، پشتیبانی کامل از Edge Runtime و سینتکس SQL-محور، واقعاً به انتخاب اول خیلی از تیم‌های حرفه‌ای تبدیل شده. من خودم بعد از کار کردن باهاش تو چند پروژه، دیگه برنگشتم سمت Prisma.

چرا Drizzle به جای Prisma؟ دلایلش ساده‌ست:

  • حجم بسیار کوچک: Drizzle حدود ۷.۴KB هست در مقابل باندل بسیار سنگین‌تر Prisma
  • پشتیبانی بومی از Edge Runtime: بدون نیاز به سرویس‌های واسط مثل Prisma Accelerate
  • صفر وابستگی: هیچ dependency خارجی نداره — بله، درست خوندید، صفر!
  • Type Safety کامل: بدون نیاز به اجرای دستور generate جداگانه. تایپ‌ها مستقیم از اسکیما استخراج میشن
  • سینتکس SQL-محور: اگه SQL بلدید، Drizzle رو هم بلدید

تو این راهنما، قراره با هم یه اپلیکیشن مدیریت وظایف (Task Manager) فول‌استک بسازیم. از راه‌اندازی پروژه و اتصال به Neon PostgreSQL شروع می‌کنیم، اسکیمای دیتابیس رو تعریف می‌کنیم، مایگریشن اجرا می‌کنیم و در نهایت عملیات CRUD کامل رو با Server Actions و اعتبارسنجی Zod پیاده‌سازی می‌کنیم.

آماده‌اید؟ بزن بریم!

پیش‌نیازها و راه‌اندازی پروژه

قبل از هر کاری، مطمئن بشید این موارد رو دارید:

  • Node.js نسخه ۱۸.۱۸ یا بالاتر
  • یه اکانت رایگان در Neon (neon.tech) برای PostgreSQL سرورلس
  • آشنایی اولیه با Next.js App Router و TypeScript

ایجاد پروژه Next.js 15

اول پروژه جدید رو بسازید:

npx create-next-app@latest task-manager
cd task-manager

موقع نصب، گزینه‌های TypeScript، ESLint، Tailwind CSS و App Router رو فعال کنید. اینا تقریباً تو هر پروژه جدی لازمتون میشه.

نصب پکیج‌های Drizzle

حالا بریم سراغ نصب پکیج‌ها:

# پکیج اصلی Drizzle ORM و درایور Neon
npm install drizzle-orm @neondatabase/serverless

# ابزارهای توسعه: Drizzle Kit برای مایگریشن و dotenv
npm install -D drizzle-kit dotenv

# Zod برای اعتبارسنجی فرم‌ها
npm install zod

ساختار پوشه‌بندی پیشنهادی ما اینطوریه:

task-manager/
├── app/
│   ├── layout.tsx
│   ├── page.tsx
│   └── tasks/
│       ├── page.tsx
│       ├── new/
│       │   └── page.tsx
│       └── [id]/
│           └── edit/
│               └── page.tsx
├── db/
│   ├── schema.ts          # تعریف اسکیمای دیتابیس
│   └── index.ts            # اتصال به دیتابیس
├── lib/
│   ├── actions.ts          # Server Actions برای CRUD
│   └── validations.ts      # اسکیماهای Zod
├── components/
│   └── TaskForm.tsx         # کامپوننت فرم
├── drizzle.config.ts        # تنظیمات Drizzle Kit
└── .env.local               # متغیرهای محیطی

این ساختار تمیز و مقیاس‌پذیره. البته می‌تونید بسته به سلیقه‌تون تغییرش بدید، ولی من این مدل رو تو چند پروژه تست کردم و جواب داده.

راه‌اندازی Neon PostgreSQL

Neon یه سرویس PostgreSQL سرورلس هست که تیر رایگانش واقعاً سخاوتمندانه‌ست. مخصوصاً برای پروژه‌های Next.js که روی Vercel دپلوی میشن عالیه.

راه‌اندازیش ساده‌ست:

  1. به neon.tech برید و یه اکانت رایگان بسازید
  2. یه پروژه جدید بسازید و منطقه نزدیک به کاربرانتون رو انتخاب کنید
  3. از بخش Connection Details رشته اتصال (Connection String) رو کپی کنید

حالا رشته اتصال رو توی فایل .env.local قرار بدید:

# .env.local
DATABASE_URL="postgresql://username:[email protected]/neondb?sslmode=require"

تعریف اسکیمای دیتابیس با Drizzle

یکی از زیبایی‌های Drizzle (و شخصاً فیچر مورد علاقه‌م) اینه که اسکیمای دیتابیس رو مستقیماً با TypeScript تعریف می‌کنید. دیگه خبری از فایل‌های .prisma جداگانه نیست — همه چیز TypeScript خالصه و تایپ‌ها به‌صورت خودکار از همون تعریف اسکیما درمیان.

فایل اسکیما

// db/schema.ts

import {
  pgTable,
  serial,
  text,
  boolean,
  timestamp,
  varchar,
  pgEnum,
} from "drizzle-orm/pg-core";

// تعریف enum برای اولویت وظایف
export const priorityEnum = pgEnum("priority", [
  "low",
  "medium",
  "high",
]);

// تعریف جدول وظایف
export const tasks = pgTable("tasks", {
  id: serial("id").primaryKey(),
  title: varchar("title", { length: 255 }).notNull(),
  description: text("description"),
  completed: boolean("completed").default(false).notNull(),
  priority: priorityEnum("priority").default("medium").notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

// استخراج تایپ‌ها از اسکیما
export type Task = typeof tasks.$inferSelect;
export type NewTask = typeof tasks.$inferInsert;

ببینید چقدر تمیزه! $inferSelect تایپ خروجی select رو میده و $inferInsert تایپ ورودی insert. دیگه نیازی نیست تایپ‌های جداگانه بنویسید و نگران sync نگه‌داشتنشون باشید.

فایل اتصال به دیتابیس

// db/index.ts

import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";

// ایجاد کلاینت Neon با درایور HTTP (بهینه برای محیط سرورلس)
const sql = neon(process.env.DATABASE_URL!);

// ایجاد نمونه Drizzle با اسکیما برای relational queries
export const db = drizzle(sql, { schema });

نکته مهم: ما اینجا از درایور HTTP نئون استفاده می‌کنیم. چرا؟ چون برای محیط‌های سرورلس (مثل Vercel) بهینه‌تره — هر کوئری یه درخواست HTTP مستقل هست و نیازی به connection pool نداره. اگه به تراکنش‌های تعاملی نیاز دارید، بعداً درباره درایور WebSocket هم صحبت می‌کنیم.

تنظیمات Drizzle Kit و اجرای مایگریشن

Drizzle Kit ابزار CLI برای مدیریت مایگریشن‌هاست. فایل تنظیماتش رو بسازید:

// drizzle.config.ts

import "dotenv/config";
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

حالا اسکریپت‌های npm رو به package.json اضافه کنید:

{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:push": "drizzle-kit push",
    "db:studio": "drizzle-kit studio"
  }
}

شاید بپرسید تفاوت این دستورات چیه؟

  • db:generate — فایل‌های SQL مایگریشن رو از تغییرات اسکیما تولید می‌کنه
  • db:migrate — مایگریشن‌های تولیدشده رو روی دیتابیس اجرا می‌کنه
  • db:push — مستقیماً اسکیما رو روی دیتابیس اعمال می‌کنه (مناسب برای توسعه سریع)
  • db:studio — رابط گرافیکی Drizzle Studio رو باز می‌کنه که واقعاً خوشگله

برای شروع سریع توسعه، از push استفاده کنید:

npm run db:push

ولی برای محیط پروداکشن، همیشه از generate و migrate استفاده کنید تا تاریخچه مایگریشن‌ها حفظ بشه. این نکته خیلی مهمه و ازش غافل نشید.

اعتبارسنجی با Zod: یک اسکیما، دو کاربرد

یکی از بهترین کارهایی که می‌تونید بکنید اینه که اسکیمای اعتبارسنجی Zod رو توی یه فایل مشترک تعریف کنید. اینطوری هم سمت کلاینت و هم سمت سرور قابل استفاده‌ست و از تکرار کد جلوگیری میشه.

// lib/validations.ts

import { z } from "zod";

export const taskSchema = z.object({
  title: z
    .string()
    .min(1, "عنوان الزامی است")
    .max(255, "عنوان حداکثر ۲۵۵ کاراکتر باشد"),
  description: z
    .string()
    .max(1000, "توضیحات حداکثر ۱۰۰۰ کاراکتر باشد")
    .optional()
    .or(z.literal("")),
  priority: z.enum(["low", "medium", "high"], {
    errorMap: () => ({ message: "اولویت نامعتبر است" }),
  }),
});

export type TaskFormData = z.infer<typeof taskSchema>;

با این روش، تایپ TaskFormData به‌صورت خودکار از اسکیمای Zod استخراج میشه. یه منبع حقیقت واحد (single source of truth) برای اعتبارسنجی و تایپ‌ها — تمیز و بدون دردسر.

Server Actions: قلب عملیات CRUD

خب، حالا بریم سراغ اصل ماجرا — Server Actions برای عملیات CRUD. اگه مقاله قبلی ما درباره واکشی داده رو خونده باشید، می‌دونید که Server Actions توابع سمت سروری هستن که مستقیماً از کامپوننت‌ها فراخوانی میشن. هیچ API route جداگانه‌ای لازم نیست.

اینجا قراره همه عملیات دیتابیسی رو با Server Actions پیاده‌سازی کنیم.

فایل کامل Server Actions

// lib/actions.ts

"use server";

import { db } from "@/db";
import { tasks } from "@/db/schema";
import { taskSchema } from "@/lib/validations";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

// تایپ وضعیت فرم برای useActionState
export type FormState = {
  errors?: {
    title?: string[];
    description?: string[];
    priority?: string[];
  };
  message?: string;
};

// ---- CREATE: ایجاد وظیفه جدید ----
export async function createTask(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  // اعتبارسنجی سمت سرور با Zod
  const validatedFields = taskSchema.safeParse({
    title: formData.get("title"),
    description: formData.get("description"),
    priority: formData.get("priority"),
  });

  // بررسی خطای اعتبارسنجی
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: "لطفاً خطاهای فرم را برطرف کنید.",
    };
  }

  try {
    await db.insert(tasks).values({
      title: validatedFields.data.title,
      description: validatedFields.data.description || null,
      priority: validatedFields.data.priority,
    });
  } catch (error) {
    return {
      message: "خطا در ایجاد وظیفه. لطفاً دوباره تلاش کنید.",
    };
  }

  revalidatePath("/tasks");
  redirect("/tasks");
}

// ---- READ: دریافت همه وظایف ----
export async function getTasks() {
  return await db
    .select()
    .from(tasks)
    .orderBy(tasks.createdAt);
}

// ---- READ: دریافت یک وظیفه ----
export async function getTaskById(id: number) {
  const result = await db
    .select()
    .from(tasks)
    .where(eq(tasks.id, id))
    .limit(1);

  return result[0] || null;
}

// ---- UPDATE: بروزرسانی وظیفه ----
export async function updateTask(
  id: number,
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  const validatedFields = taskSchema.safeParse({
    title: formData.get("title"),
    description: formData.get("description"),
    priority: formData.get("priority"),
  });

  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: "لطفاً خطاهای فرم را برطرف کنید.",
    };
  }

  try {
    await db
      .update(tasks)
      .set({
        title: validatedFields.data.title,
        description: validatedFields.data.description || null,
        priority: validatedFields.data.priority,
        updatedAt: new Date(),
      })
      .where(eq(tasks.id, id));
  } catch (error) {
    return {
      message: "خطا در بروزرسانی وظیفه. لطفاً دوباره تلاش کنید.",
    };
  }

  revalidatePath("/tasks");
  redirect("/tasks");
}

// ---- DELETE: حذف وظیفه ----
export async function deleteTask(id: number) {
  try {
    await db.delete(tasks).where(eq(tasks.id, id));
  } catch (error) {
    return { message: "خطا در حذف وظیفه." };
  }

  revalidatePath("/tasks");
}

// ---- TOGGLE: تغییر وضعیت تکمیل ----
export async function toggleTaskComplete(id: number) {
  const task = await getTaskById(id);
  if (!task) return { message: "وظیفه یافت نشد." };

  try {
    await db
      .update(tasks)
      .set({
        completed: !task.completed,
        updatedAt: new Date(),
      })
      .where(eq(tasks.id, id));
  } catch (error) {
    return { message: "خطا در تغییر وضعیت." };
  }

  revalidatePath("/tasks");
}

چند نکته مهم درباره این کد که حتماً باید بدونید:

  • اعتبارسنجی سمت سرور: همیشه، بدون استثنا، ورودی‌ها رو سمت سرور اعتبارسنجی کنید. اعتبارسنجی کلاینت فقط برای UX بهتره و به‌تنهایی کافی نیست
  • مدیریت خطا: هیچ‌وقت خطاهای داخلی دیتابیس رو به کاربر نشون ندید. پیام‌های عمومی برگردونید و خطاهای دقیق رو فقط سمت سرور لاگ کنید
  • revalidatePath: بعد از هر mutation حتماً کش مسیر مربوطه رو revalidate کنید وگرنه داده‌های قدیمی نمایش داده میشن
  • redirect: بعد از mutation موفق، کاربر رو به صفحه مقصد هدایت کنید

ساخت رابط کاربری با Server Components و useActionState

حالا بریم سراغ ساخت رابط کاربری. یکی از تغییرات مهم React 19 و Next.js 15 معرفی هوک useActionState بود که جایگزین useFormState قدیمی شد. راستش این هوک مدیریت وضعیت فرم رو خیلی ساده‌تر کرده.

کامپوننت فرم (Client Component)

// components/TaskForm.tsx

"use client";

import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import type { FormState } from "@/lib/actions";
import type { Task } from "@/db/schema";

// دکمه ارسال جداگانه برای استفاده از useFormStatus
function SubmitButton({ label }: { label: string }) {
  const { pending } = useFormStatus();

  return (
    <button
      type="submit"
      disabled={pending}
      className="bg-blue-600 text-white px-4 py-2 rounded
                 disabled:opacity-50 disabled:cursor-not-allowed"
    >
      {pending ? "در حال ارسال..." : label}
    </button>
  );
}

type Props = {
  action: (
    prevState: FormState,
    formData: FormData
  ) => Promise<FormState>;
  initialData?: Task;
  submitLabel: string;
};

export default function TaskForm({
  action,
  initialData,
  submitLabel,
}: Props) {
  const [state, formAction] = useActionState(action, {});

  return (
    <form action={formAction} className="space-y-4">
      {/* فیلد عنوان */}
      <div>
        <label htmlFor="title">عنوان</label>
        <input
          id="title"
          name="title"
          type="text"
          defaultValue={initialData?.title}
          className="w-full border rounded px-3 py-2"
        />
        {state.errors?.title && (
          <p className="text-red-500 text-sm">
            {state.errors.title[0]}
          </p>
        )}
      </div>

      {/* فیلد توضیحات */}
      <div>
        <label htmlFor="description">توضیحات</label>
        <textarea
          id="description"
          name="description"
          defaultValue={initialData?.description || ""}
          rows={4}
          className="w-full border rounded px-3 py-2"
        />
        {state.errors?.description && (
          <p className="text-red-500 text-sm">
            {state.errors.description[0]}
          </p>
        )}
      </div>

      {/* فیلد اولویت */}
      <div>
        <label htmlFor="priority">اولویت</label>
        <select
          id="priority"
          name="priority"
          defaultValue={initialData?.priority || "medium"}
          className="w-full border rounded px-3 py-2"
        >
          <option value="low">کم</option>
          <option value="medium">متوسط</option>
          <option value="high">بالا</option>
        </select>
        {state.errors?.priority && (
          <p className="text-red-500 text-sm">
            {state.errors.priority[0]}
          </p>
        )}
      </div>

      {/* پیام خطای عمومی */}
      {state.message && (
        <p className="text-red-500">{state.message}</p>
      )}

      <SubmitButton label={submitLabel} />
    </form>
  );
}

نکات کلیدی این کامپوننت:

  • useActionState وضعیت برگشتی از Server Action رو مدیریت می‌کنه و خطاهای فیلدی رو نمایش میده
  • useFormStatus باید توی یه کامپوننت فرزند فرم استفاده بشه (نه خود فرم) — به همین دلیل SubmitButton رو جدا نوشتیم
  • از defaultValue به جای value استفاده کردیم تا فرم uncontrolled باشه و با Server Actions سازگاری داشته باشه

صفحه لیست وظایف (Server Component)

// app/tasks/page.tsx

import { getTasks, deleteTask, toggleTaskComplete } from "@/lib/actions";
import Link from "next/link";

export default async function TasksPage() {
  const taskList = await getTasks();

  return (
    <div className="max-w-2xl mx-auto p-6">
      <div className="flex justify-between items-center mb-6">
        <h1 className="text-2xl font-bold">وظایف من</h1>
        <Link
          href="/tasks/new"
          className="bg-blue-600 text-white px-4 py-2 rounded"
        >
          وظیفه جدید
        </Link>
      </div>

      {taskList.length === 0 ? (
        <p className="text-gray-500">هنوز وظیفه‌ای ثبت نشده.</p>
      ) : (
        <ul className="space-y-3">
          {taskList.map((task) => (
            <li
              key={task.id}
              className="border rounded p-4 flex items-center
                         justify-between"
            >
              <div className="flex items-center gap-3">
                <form action={toggleTaskComplete.bind(null, task.id)}>
                  <button type="submit">
                    {task.completed ? "✅" : "⬜"}
                  </button>
                </form>
                <div>
                  <h2 className={task.completed ? "line-through" : ""}>
                    {task.title}
                  </h2>
                  <span className="text-sm text-gray-500">
                    {task.priority}
                  </span>
                </div>
              </div>
              <div className="flex gap-2">
                <Link href={`/tasks/${task.id}/edit`}>ویرایش</Link>
                <form action={deleteTask.bind(null, task.id)}>
                  <button type="submit" className="text-red-500">
                    حذف
                  </button>
                </form>
              </div>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

دقت کنید که این صفحه یه Server Component هست. داده مستقیماً روی سرور واکشی میشه و HTML رندرشده به کلاینت فرستاده میشه. هیچ useEffect یا loading state ندارید — و همین سادگیش رو دوست‌داشتنی می‌کنه.

برای عملیات delete و toggle از .bind(null, task.id) استفاده کردیم تا id رو به Server Action پاس بدیم. این یه الگوی استاندارد تو Next.js 15 هست.

صفحه ایجاد وظیفه جدید

// app/tasks/new/page.tsx

import TaskForm from "@/components/TaskForm";
import { createTask } from "@/lib/actions";

export default function NewTaskPage() {
  return (
    <div className="max-w-lg mx-auto p-6">
      <h1 className="text-2xl font-bold mb-4">ایجاد وظیفه جدید</h1>
      <TaskForm action={createTask} submitLabel="ایجاد" />
    </div>
  );
}

صفحه ویرایش وظیفه

// app/tasks/[id]/edit/page.tsx

import { notFound } from "next/navigation";
import TaskForm from "@/components/TaskForm";
import { getTaskById, updateTask } from "@/lib/actions";

type Props = {
  params: Promise<{ id: string }>;
};

export default async function EditTaskPage({ params }: Props) {
  const { id } = await params;
  const task = await getTaskById(Number(id));

  if (!task) notFound();

  // bind کردن id به action برای بروزرسانی
  const updateTaskWithId = updateTask.bind(null, task.id);

  return (
    <div className="max-w-lg mx-auto p-6">
      <h1 className="text-2xl font-bold mb-4">ویرایش وظیفه</h1>
      <TaskForm
        action={updateTaskWithId}
        initialData={task}
        submitLabel="بروزرسانی"
      />
    </div>
  );
}

یه نکته: در Next.js 15 پارامتر params حالا یه Promise هست و باید await بشه. اگه از نسخه‌های قبلی مهاجرت می‌کنید، احتمالاً با این تغییر مواجه میشید.

الگوهای پیشرفته: فیلتر، جستجو و صفحه‌بندی

تا اینجا CRUD پایه رو ساختیم. ولی خب، یه اپلیکیشن واقعی فقط CRUD ساده نیست، نه؟ بیایید چند الگوی پیشرفته‌تر رو هم ببینیم.

فیلتر و جستجو با کوئری‌های Drizzle

// lib/actions.ts — اضافه کردن تابع جستجو

import { and, ilike, eq, desc, sql } from "drizzle-orm";

export async function searchTasks(params: {
  query?: string;
  priority?: "low" | "medium" | "high";
  completed?: boolean;
  page?: number;
  pageSize?: number;
}) {
  const {
    query,
    priority,
    completed,
    page = 1,
    pageSize = 10,
  } = params;

  // ساخت شرایط فیلتر به‌صورت داینامیک
  const conditions = [];

  if (query) {
    conditions.push(ilike(tasks.title, `%${query}%`));
  }
  if (priority) {
    conditions.push(eq(tasks.priority, priority));
  }
  if (completed !== undefined) {
    conditions.push(eq(tasks.completed, completed));
  }

  const offset = (page - 1) * pageSize;

  // اجرای کوئری با فیلتر و صفحه‌بندی
  const [items, countResult] = await Promise.all([
    db
      .select()
      .from(tasks)
      .where(and(...conditions))
      .orderBy(desc(tasks.createdAt))
      .limit(pageSize)
      .offset(offset),
    db
      .select({ count: sql<number>`count(*)` })
      .from(tasks)
      .where(and(...conditions)),
  ]);

  return {
    items,
    totalCount: countResult[0].count,
    totalPages: Math.ceil(countResult[0].count / pageSize),
    currentPage: page,
  };
}

اینجا از Promise.all برای اجرای موازی دو کوئری استفاده کردیم — یکی برای دریافت آیتم‌ها و یکی برای شمارش کل نتایج. سینتکس Drizzle واقعاً شبیه SQL خالصه و اگه با SQL آشنا باشید، نوشتن این کوئری‌ها براتون مثل آب خوردن میشه.

بهترین شیوه‌ها و نکات پروداکشن

قبل از اینکه اپلیکیشنتون رو دپلوی کنید، حتماً این نکات رو رعایت کنید. بعضیاشون شاید بدیهی به نظر برسن ولی تجربه نشون داده آدم‌ها ازشون غافل میشن.

۱. همیشه از مایگریشن استفاده کنید

تو محیط توسعه، db:push سریع و راحته. ولی تو پروداکشن حتماً از db:generate و db:migrate استفاده کنید. فایل‌های مایگریشن SQL رو commit کنید تا تاریخچه تغییرات دیتابیس قابل بازبینی باشه.

۲. مدیریت صحیح خطاها

هیچ‌وقت خطاهای داخلی دیتابیس رو مستقیم به کاربر نشون ندید. تو Server Actions، خطاها رو بگیرید، سمت سرور لاگ کنید و پیام عمومی برگردونید:

try {
  await db.insert(tasks).values(data);
} catch (error) {
  console.error("Database error:", error);
  return { message: "خطای سرور. لطفاً دوباره تلاش کنید." };
}

۳. revalidation بعد از هر mutation

بعد از هر عملیات نوشتن (insert, update, delete) حتماً revalidatePath رو فراخوانی کنید. بدون این کار، Next.js همچنان نسخه کش‌شده قبلی رو نمایش میده و کاربر فکر می‌کنه تغییراتش اعمال نشده.

۴. استفاده از تراکنش‌ها برای عملیات ترکیبی

اگه چند عملیات دیتابیسی وابسته به هم دارید، از تراکنش استفاده کنید. برای این کار باید از درایور WebSocket نئون استفاده کنید:

import { Pool } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-serverless";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});
const dbPool = drizzle(pool);

// استفاده از تراکنش
await dbPool.transaction(async (tx) => {
  await tx.insert(tasks).values(newTask);
  await tx.update(projects).set({ taskCount: sql`task_count + 1` })
    .where(eq(projects.id, projectId));
});

۵. متغیرهای محیطی و امنیت

رشته اتصال دیتابیس رو هرگز توی کد هاردکد نکنید. همیشه از متغیرهای محیطی استفاده کنید و فایل .env.local رو توی .gitignore قرار بدید. این موضوع شاید بدیهی باشه ولی تعداد ریپوهایی که connection string توشون push شده واقعاً تعجب‌آوره.

دپلوی روی Vercel

دپلوی این اپلیکیشن روی Vercel خوشبختانه خیلی ساده‌ست:

  1. ریپوی پروژه رو به GitHub پوش کنید
  2. در داشبورد Vercel، پروژه جدید بسازید و ریپو رو وصل کنید
  3. متغیر محیطی DATABASE_URL رو در تنظیمات Vercel وارد کنید
  4. دپلوی رو اجرا کنید — همین!

قبل از دپلوی، مایگریشن‌ها رو اجرا کنید. می‌تونید از اسکریپت build استفاده کنید:

{
  "scripts": {
    "build": "drizzle-kit migrate && next build"
  }
}

چون Drizzle بدون هیچ وابستگی باینری کار می‌کنه، مشکلات رایج Prisma در محیط سرورلس (مثل حجم باندل بالا یا عدم سازگاری با Edge Runtime) رو اصلاً ندارید. و این یکی از بزرگ‌ترین مزیت‌هاشه.

سوالات متداول

آیا Drizzle ORM برای پروژه‌های بزرگ مناسبه؟

قطعاً بله. Drizzle تو پروداکشن شرکت‌های بزرگ استفاده میشه و با بیش از ۲۵ هزار ستاره GitHub و جامعه فعال، یه انتخاب مطمئنه. حجم کم و صفر وابستگی خارجی اون رو برای محیط‌های سرورلس ایده‌آل می‌کنه.

تفاوت db:push و db:migrate چیه و کدوم رو استفاده کنم؟

دستور db:push مستقیماً اسکیما رو روی دیتابیس اعمال می‌کنه و برای توسعه محلی سریعه. اما db:generate و db:migrate فایل‌های SQL مایگریشن تولید و اجرا می‌کنن و تاریخچه تغییرات رو حفظ می‌کنن. قاعده ساده: توسعه = push، پروداکشن = migrate.

آیا Drizzle ORM از Edge Runtime پشتیبانی می‌کنه؟

بله، و این یکی از بزرگ‌ترین مزیت‌هاش نسبت به Prisma هست. Prisma برای Edge Runtime به سرویس Accelerate نیاز داره، ولی Drizzle به‌صورت بومی ازش پشتیبانی می‌کنه. یعنی می‌تونید تو Middleware، Edge Functions و هر محیط Edge بدون تنظیمات اضافی ازش استفاده کنید.

چطور از ایجاد اتصالات زیاد به دیتابیس جلوگیری کنم؟

با درایور HTTP نئون (neon-http) هر کوئری یه درخواست HTTP مستقل هست و اصلاً نیازی به connection pool نداره. اگه از درایور WebSocket استفاده می‌کنید، از Pool نئون استفاده کنید و نمونه db رو به‌صورت singleton مدیریت کنید.

آیا می‌تونم Drizzle رو با دیتابیس‌های دیگه غیر از PostgreSQL استفاده کنم؟

بله! Drizzle از PostgreSQL، MySQL و SQLite پشتیبانی می‌کنه. همچنین با دیتابیس‌های سرورلس مثل Turso، PlanetScale، Cloudflare D1 و Supabase هم سازگاره. تنها تفاوتی که هست در درایور اتصال و نوع ستون‌هاست — بقیه سینتکس تقریباً یکسانه.

درباره نویسنده Editorial Team

Our team of expert writers and editors.