بناء تطبيق CRUD متكامل باستخدام MongoDB و Mongoose و Next.js 15

MongoDB هي قاعدة البيانات NoSQL الأكثر شعبية في العالم — و Mongoose يجعلها آمنة وأنيقة في TypeScript. في هذا الدليل ستبني تطبيق إدارة مهام متكامل من الصفر باستخدام MongoDB Atlas و Mongoose ODM و Next.js 15 App Router مع Server Actions.
ماذا ستتعلم
بنهاية هذا الدليل، ستكون قادرًا على:
- إعداد مجموعة MongoDB Atlas وربطها بـ Next.js
- تعريف مخططات Mongoose مع استدلال أنواع TypeScript
- بناء عمليات CRUD كاملة باستخدام Next.js Server Actions
- تنفيذ التحقق من النماذج باستخدام Zod
- إضافة البحث والتصفية مع استعلامات MongoDB
- التعامل مع التقسيم إلى صفحات
- النشر في بيئة الإنتاج مع أفضل ممارسات إدارة الاتصالات
المتطلبات المسبقة
قبل البدء، تأكد من توفر:
- Node.js 20+ (
node --version) - خبرة في TypeScript (الأنواع، الواجهات، async/await)
- معرفة بـ Next.js (App Router, Server Components)
- حساب MongoDB Atlas (الطبقة المجانية تعمل بشكل ممتاز)
- محرر أكواد — يُنصح بـ VS Code أو Cursor
لماذا MongoDB + Mongoose؟
MongoDB هي قاعدة بيانات مستندات تخزن البيانات في مستندات مرنة شبيهة بـ JSON. مع Mongoose، تحصل على التحقق من المخططات وأمان الأنواع وواجهة استعلام قوية. إليك المقارنة:
| الميزة | MongoDB + Mongoose | PostgreSQL + Prisma | SQLite + Drizzle |
|---|---|---|---|
| نموذج البيانات | مستندات (JSON) | جداول علائقية | جداول علائقية |
| المخطط | مرن، اختياري | صارم، مطلوب | صارم، مطلوب |
| أمان الأنواع | Mongoose + TS generics | أنواع مُولّدة تلقائيًا | أنواع مُستنتجة |
| التوسع | أفقي (sharding) | عمودي (read replicas) | ملف واحد |
| البيانات المتداخلة | تضمين أصلي | أعمدة JSON أو joins | أعمدة JSON أو joins |
| الطبقة المجانية | Atlas 512 MB للأبد | Neon 0.5 GB | محلي، غير محدود |
اختر MongoDB عندما تكون بياناتك هرمية بطبيعتها، أو يتطور مخططك بشكل متكرر، أو تحتاج إلى توسع أفقي.
الخطوة 1: إنشاء مشروع Next.js
أنشئ مشروع Next.js 15 جديد مع TypeScript و Tailwind CSS:
npx create-next-app@latest task-manager --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd task-managerثبّت حزم MongoDB و Mongoose:
npm install mongoose zod
npm install -D @types/mongooseهيكل المشروع سيكون كالتالي:
task-manager/
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── tasks/
│ ├── lib/
│ │ ├── mongodb.ts
│ │ └── actions/
│ └── models/
│ └── task.ts
├── .env.local
└── package.json
الخطوة 2: إعداد MongoDB Atlas
إنشاء مجموعة مجانية
- اذهب إلى MongoDB Atlas وسجّل الدخول
- انقر على Build a Database واختر طبقة M0 Free
- اختر مزود السحابة والمنطقة الأقرب لمستخدميك
- انقر على Create Deployment
إعداد الوصول
- أنشئ مستخدم قاعدة بيانات باسم مستخدم وكلمة مرور
- في Network Access، أضف عنوان IP الخاص بك أو Allow Access from Anywhere للتطوير
- انقر على Connect واختر Drivers وانسخ سلسلة الاتصال
إضافة سلسلة الاتصال
أنشئ ملف .env.local في جذر المشروع:
MONGODB_URI=mongodb+srv://<username>:<password>@<cluster>.mongodb.net/task-manager?retryWrites=true&w=majorityاستبدل <username> و <password> و <cluster> ببيانات Atlas الفعلية.
الخطوة 3: إنشاء أداة اتصال MongoDB
اتصالات MongoDB في بيئات serverless تحتاج معالجة خاصة. يجب تخزين الاتصال مؤقتًا لتجنب استنفاد حوض الاتصالات.
أنشئ src/lib/mongodb.ts:
import mongoose from "mongoose";
const MONGODB_URI = process.env.MONGODB_URI!;
if (!MONGODB_URI) {
throw new Error("Please define the MONGODB_URI environment variable in .env.local");
}
interface MongooseCache {
conn: typeof mongoose | null;
promise: Promise<typeof mongoose> | null;
}
declare global {
var mongooseCache: MongooseCache | undefined;
}
const cached: MongooseCache = global.mongooseCache ?? { conn: null, promise: null };
if (!global.mongooseCache) {
global.mongooseCache = cached;
}
export async function connectDB(): Promise<typeof mongoose> {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const opts = {
bufferCommands: false,
maxPoolSize: 10,
};
cached.promise = mongoose.connect(MONGODB_URI, opts).then((m) => m);
}
cached.conn = await cached.promise;
return cached.conn;
}هذا النمط يخزّن الاتصال مؤقتًا عبر عمليات إعادة التحميل الساخنة في التطوير وعبر استدعاءات الدوال في الإنتاج. المتغير global يبقى محفوظًا عبر عمليات إعادة تحميل وحدات Next.js.
الخطوة 4: تعريف مخطط Mongoose
مخططات Mongoose تحدد شكل المستندات وتوفر التحقق والقيم الافتراضية وخطافات البرمجيات الوسيطة.
أنشئ src/models/task.ts:
import mongoose, { Schema, Document, Model } from "mongoose";
export interface ITask {
title: string;
description: string;
status: "todo" | "in-progress" | "done";
priority: "low" | "medium" | "high";
dueDate?: Date;
tags: string[];
createdAt: Date;
updatedAt: Date;
}
export interface ITaskDocument extends ITask, Document {}
const taskSchema = new Schema<ITaskDocument>(
{
title: {
type: String,
required: [true, "Title is required"],
trim: true,
maxlength: [200, "Title cannot exceed 200 characters"],
},
description: {
type: String,
required: [true, "Description is required"],
trim: true,
maxlength: [2000, "Description cannot exceed 2000 characters"],
},
status: {
type: String,
enum: ["todo", "in-progress", "done"],
default: "todo",
},
priority: {
type: String,
enum: ["low", "medium", "high"],
default: "medium",
},
dueDate: {
type: Date,
},
tags: {
type: [String],
default: [],
},
},
{
timestamps: true,
}
);
taskSchema.index({ status: 1, priority: 1 });
taskSchema.index({ title: "text", description: "text" });
taskSchema.index({ createdAt: -1 });
const Task: Model<ITaskDocument> =
mongoose.models.Task || mongoose.model<ITaskDocument>("Task", taskSchema);
export default Task;قرارات التصميم الرئيسية:
timestamps: trueيدير تلقائيًاcreatedAtوupdatedAt- الفهارس على
statusوpriorityتسرّع الاستعلامات المُفلترة - فهرس النص على
titleوdescriptionيُمكّن البحث النصي الكامل mongoose.models.Task ||يمنع إعادة تجميع النموذج أثناء إعادة التحميل الساخن
الخطوة 5: إنشاء مخططات التحقق بـ Zod
عرّف مخططات التحقق التي تعمل على الخادم والعميل.
أنشئ src/lib/validations/task.ts:
import { z } from "zod";
export const createTaskSchema = z.object({
title: z
.string()
.min(1, "Title is required")
.max(200, "Title cannot exceed 200 characters"),
description: z
.string()
.min(1, "Description is required")
.max(2000, "Description cannot exceed 2000 characters"),
status: z.enum(["todo", "in-progress", "done"]).default("todo"),
priority: z.enum(["low", "medium", "high"]).default("medium"),
dueDate: z.string().optional().transform((val) => (val ? new Date(val) : undefined)),
tags: z
.string()
.optional()
.transform((val) => (val ? val.split(",").map((t) => t.trim()).filter(Boolean) : [])),
});
export const updateTaskSchema = createTaskSchema.partial();
export type CreateTaskInput = z.infer<typeof createTaskSchema>;
export type UpdateTaskInput = z.infer<typeof updateTaskSchema>;الخطوة 6: بناء Server Actions
Server Actions هي الطريقة الأنظف للتعامل مع إرسال النماذج وتعديل البيانات في Next.js 15. تعمل على الخادم ويمكنها الوصول مباشرة إلى قاعدة البيانات.
أنشئ src/lib/actions/task-actions.ts:
"use server";
import { revalidatePath } from "next/cache";
import { connectDB } from "@/lib/mongodb";
import Task from "@/models/task";
import { createTaskSchema, updateTaskSchema } from "@/lib/validations/task";
export type ActionState = {
success: boolean;
message: string;
errors?: Record<string, string[]>;
};
export async function createTask(
_prevState: ActionState,
formData: FormData
): Promise<ActionState> {
await connectDB();
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,
tags: formData.get("tags") as string,
};
const result = createTaskSchema.safeParse(raw);
if (!result.success) {
return {
success: false,
message: "فشل التحقق من البيانات",
errors: result.error.flatten().fieldErrors,
};
}
await Task.create(result.data);
revalidatePath("/tasks");
return { success: true, message: "تم إنشاء المهمة بنجاح" };
}
export async function updateTask(
id: string,
_prevState: ActionState,
formData: FormData
): Promise<ActionState> {
await connectDB();
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,
tags: formData.get("tags") as string,
};
const result = updateTaskSchema.safeParse(raw);
if (!result.success) {
return {
success: false,
message: "فشل التحقق من البيانات",
errors: result.error.flatten().fieldErrors,
};
}
const task = await Task.findByIdAndUpdate(id, result.data, { new: true });
if (!task) {
return { success: false, message: "لم يتم العثور على المهمة" };
}
revalidatePath("/tasks");
return { success: true, message: "تم تحديث المهمة بنجاح" };
}
export async function deleteTask(id: string): Promise<ActionState> {
await connectDB();
const task = await Task.findByIdAndDelete(id);
if (!task) {
return { success: false, message: "لم يتم العثور على المهمة" };
}
revalidatePath("/tasks");
return { success: true, message: "تم حذف المهمة بنجاح" };
}
export async function toggleTaskStatus(
id: string,
newStatus: "todo" | "in-progress" | "done"
): Promise<ActionState> {
await connectDB();
const task = await Task.findByIdAndUpdate(id, { status: newStatus }, { new: true });
if (!task) {
return { success: false, message: "لم يتم العثور على المهمة" };
}
revalidatePath("/tasks");
return { success: true, message: "تم تحديث الحالة" };
}كل إجراء يتبع نمطًا ثابتًا: اتصال، تحقق، تنفيذ، إعادة تحقق. النوع ActionState يوفر شكلاً متوقعًا لمعالجة النتائج على العميل.
الخطوة 7: بناء طبقة الوصول للبيانات
أنشئ دوال الاستعلام لقراءة البيانات. هذه تعمل في Server Components.
أنشئ src/lib/actions/task-queries.ts:
import { connectDB } from "@/lib/mongodb";
import Task, { ITaskDocument } from "@/models/task";
export interface TaskFilters {
status?: string;
priority?: string;
search?: string;
page?: number;
limit?: number;
}
export interface PaginatedTasks {
tasks: SerializedTask[];
total: number;
page: number;
totalPages: number;
}
export interface SerializedTask {
_id: string;
title: string;
description: string;
status: "todo" | "in-progress" | "done";
priority: "low" | "medium" | "high";
dueDate: string | null;
tags: string[];
createdAt: string;
updatedAt: string;
}
function serializeTask(task: ITaskDocument): SerializedTask {
return {
_id: task._id.toString(),
title: task.title,
description: task.description,
status: task.status,
priority: task.priority,
dueDate: task.dueDate ? task.dueDate.toISOString() : null,
tags: task.tags,
createdAt: task.createdAt.toISOString(),
updatedAt: task.updatedAt.toISOString(),
};
}
export async function getTasks(filters: TaskFilters = {}): Promise<PaginatedTasks> {
await connectDB();
const { status, priority, search, page = 1, limit = 10 } = filters;
const query: Record<string, unknown> = {};
if (status && status !== "all") {
query.status = status;
}
if (priority && priority !== "all") {
query.priority = priority;
}
if (search) {
query.$text = { $search: search };
}
const skip = (page - 1) * limit;
const [tasks, total] = await Promise.all([
Task.find(query).sort({ createdAt: -1 }).skip(skip).limit(limit).lean(),
Task.countDocuments(query),
]);
return {
tasks: (tasks as unknown as ITaskDocument[]).map(serializeTask),
total,
page,
totalPages: Math.ceil(total / limit),
};
}
export async function getTaskById(id: string): Promise<SerializedTask | null> {
await connectDB();
const task = await Task.findById(id).lean();
if (!task) return null;
return serializeTask(task as unknown as ITaskDocument);
}
export async function getTaskStats() {
await connectDB();
const [total, todo, inProgress, done] = await Promise.all([
Task.countDocuments(),
Task.countDocuments({ status: "todo" }),
Task.countDocuments({ status: "in-progress" }),
Task.countDocuments({ status: "done" }),
]);
return { total, todo, inProgress, done };
}دالة serializeTask تحوّل مستندات Mongoose إلى كائنات عادية — وهذا ضروري لأن Server Components لا يمكنها تمرير نسخ Mongoose إلى Client Components.
الخطوة 8: بناء صفحة قائمة المهام
أنشئ صفحة المهام الرئيسية كـ Server Component.
أنشئ src/app/tasks/page.tsx:
import Link from "next/link";
import { getTasks, getTaskStats } from "@/lib/actions/task-queries";
import { TaskCard } from "./task-card";
import { TaskFilters } from "./task-filters";
import { Pagination } from "./pagination";
interface PageProps {
searchParams: Promise<{
status?: string;
priority?: string;
search?: string;
page?: string;
}>;
}
export default async function TasksPage({ searchParams }: PageProps) {
const params = await searchParams;
const page = parseInt(params.page || "1", 10);
const [{ tasks, total, totalPages }, stats] = await Promise.all([
getTasks({
status: params.status,
priority: params.priority,
search: params.search,
page,
limit: 10,
}),
getTaskStats(),
]);
return (
<div className="max-w-5xl mx-auto px-4 py-8">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold">المهام</h1>
<p className="text-gray-500 mt-1">
{stats.total} إجمالي · {stats.todo} للتنفيذ ·{" "}
{stats.inProgress} قيد التنفيذ · {stats.done} مكتملة
</p>
</div>
<Link
href="/tasks/new"
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition"
>
مهمة جديدة
</Link>
</div>
<TaskFilters />
{tasks.length === 0 ? (
<div className="text-center py-16 text-gray-500">
<p className="text-lg">لم يتم العثور على مهام</p>
<p className="mt-2">أنشئ مهمتك الأولى للبدء.</p>
</div>
) : (
<div className="space-y-4 mt-6">
{tasks.map((task) => (
<TaskCard key={task._id} task={task} />
))}
</div>
)}
{totalPages > 1 && (
<Pagination currentPage={page} totalPages={totalPages} />
)}
</div>
);
}الخطوة 9: بناء مكوّن بطاقة المهمة
أنشئ Client Component لكل مهمة مع تبديل الحالة وإجراءات الحذف.
أنشئ src/app/tasks/task-card.tsx:
"use client";
import { useTransition } from "react";
import Link from "next/link";
import { deleteTask, toggleTaskStatus } from "@/lib/actions/task-actions";
import type { SerializedTask } from "@/lib/actions/task-queries";
const statusColors = {
todo: "bg-gray-100 text-gray-800",
"in-progress": "bg-blue-100 text-blue-800",
done: "bg-green-100 text-green-800",
};
const priorityColors = {
low: "bg-slate-100 text-slate-700",
medium: "bg-yellow-100 text-yellow-800",
high: "bg-red-100 text-red-800",
};
const nextStatus: Record<string, "todo" | "in-progress" | "done"> = {
todo: "in-progress",
"in-progress": "done",
done: "todo",
};
export function TaskCard({ task }: { task: SerializedTask }) {
const [isPending, startTransition] = useTransition();
const handleStatusToggle = () => {
startTransition(async () => {
await toggleTaskStatus(task._id, nextStatus[task.status]);
});
};
const handleDelete = () => {
if (!confirm("هل أنت متأكد من حذف هذه المهمة؟")) return;
startTransition(async () => {
await deleteTask(task._id);
});
};
return (
<div
className={`border rounded-lg p-4 transition ${
isPending ? "opacity-50" : ""
}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<button
onClick={handleStatusToggle}
className={`px-2 py-1 rounded-full text-xs font-medium ${
statusColors[task.status]
}`}
>
{task.status}
</button>
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${
priorityColors[task.priority]
}`}
>
{task.priority}
</span>
</div>
<Link href={`/tasks/${task._id}`} className="group">
<h3 className="text-lg font-semibold group-hover:text-blue-600 transition">
{task.title}
</h3>
</Link>
<p className="text-gray-600 mt-1 line-clamp-2">{task.description}</p>
{task.tags.length > 0 && (
<div className="flex gap-1 mt-2">
{task.tags.map((tag) => (
<span
key={tag}
className="bg-gray-100 text-gray-600 px-2 py-0.5 rounded text-xs"
>
{tag}
</span>
))}
</div>
)}
</div>
<div className="flex items-center gap-2 ml-4">
<Link
href={`/tasks/${task._id}/edit`}
className="text-gray-400 hover:text-blue-600 transition"
>
تعديل
</Link>
<button
onClick={handleDelete}
className="text-gray-400 hover:text-red-600 transition"
>
حذف
</button>
</div>
</div>
</div>
);
}الخطوة 10: بناء نموذج المهمة
أنشئ مكوّن نموذج قابل لإعادة الاستخدام لإنشاء وتعديل المهام.
أنشئ src/app/tasks/task-form.tsx:
"use client";
import { useActionState } from "react";
import { createTask, updateTask, ActionState } from "@/lib/actions/task-actions";
import type { SerializedTask } from "@/lib/actions/task-queries";
const initialState: ActionState = {
success: false,
message: "",
};
export function TaskForm({ task }: { task?: SerializedTask }) {
const action = task
? updateTask.bind(null, task._id)
: createTask;
const [state, formAction, isPending] = useActionState(action, initialState);
return (
<form action={formAction} className="space-y-6 max-w-2xl">
{state.message && !state.success && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{state.message}
</div>
)}
{state.success && (
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded">
{state.message}
</div>
)}
<div>
<label htmlFor="title" className="block text-sm font-medium mb-1">
العنوان
</label>
<input
id="title"
name="title"
type="text"
defaultValue={task?.title}
required
className="w-full border rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="أدخل عنوان المهمة"
/>
{state.errors?.title && (
<p className="text-red-500 text-sm mt-1">{state.errors.title[0]}</p>
)}
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium mb-1">
الوصف
</label>
<textarea
id="description"
name="description"
defaultValue={task?.description}
required
rows={4}
className="w-full border rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="صف المهمة"
/>
{state.errors?.description && (
<p className="text-red-500 text-sm mt-1">
{state.errors.description[0]}
</p>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="status" className="block text-sm font-medium mb-1">
الحالة
</label>
<select
id="status"
name="status"
defaultValue={task?.status || "todo"}
className="w-full border rounded-lg px-3 py-2"
>
<option value="todo">للتنفيذ</option>
<option value="in-progress">قيد التنفيذ</option>
<option value="done">مكتملة</option>
</select>
</div>
<div>
<label htmlFor="priority" className="block text-sm font-medium mb-1">
الأولوية
</label>
<select
id="priority"
name="priority"
defaultValue={task?.priority || "medium"}
className="w-full border rounded-lg px-3 py-2"
>
<option value="low">منخفضة</option>
<option value="medium">متوسطة</option>
<option value="high">عالية</option>
</select>
</div>
</div>
<div>
<label htmlFor="dueDate" className="block text-sm font-medium mb-1">
تاريخ الاستحقاق (اختياري)
</label>
<input
id="dueDate"
name="dueDate"
type="date"
defaultValue={task?.dueDate ? task.dueDate.split("T")[0] : ""}
className="w-full border rounded-lg px-3 py-2"
/>
</div>
<div>
<label htmlFor="tags" className="block text-sm font-medium mb-1">
الوسوم (مفصولة بفواصل)
</label>
<input
id="tags"
name="tags"
type="text"
defaultValue={task?.tags.join(", ")}
className="w-full border rounded-lg px-3 py-2"
placeholder="frontend, urgent, bug"
/>
</div>
<button
type="submit"
disabled={isPending}
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 transition"
>
{isPending
? task
? "جاري التحديث..."
: "جاري الإنشاء..."
: task
? "تحديث المهمة"
: "إنشاء المهمة"}
</button>
</form>
);
}الخطوة 11: بناء مكوّن التصفية
أنشئ src/app/tasks/task-filters.tsx:
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useState } from "react";
export function TaskFilters() {
const router = useRouter();
const searchParams = useSearchParams();
const [search, setSearch] = useState(searchParams.get("search") || "");
const updateFilter = useCallback(
(key: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value && value !== "all") {
params.set(key, value);
} else {
params.delete(key);
}
params.delete("page");
router.push(`/tasks?${params.toString()}`);
},
[router, searchParams]
);
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
updateFilter("search", search);
};
return (
<div className="flex flex-wrap items-center gap-4">
<form onSubmit={handleSearch} className="flex gap-2">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="ابحث في المهام..."
className="border rounded-lg px-3 py-2 w-64"
/>
<button
type="submit"
className="bg-gray-100 px-3 py-2 rounded-lg hover:bg-gray-200 transition"
>
بحث
</button>
</form>
<select
value={searchParams.get("status") || "all"}
onChange={(e) => updateFilter("status", e.target.value)}
className="border rounded-lg px-3 py-2"
>
<option value="all">كل الحالات</option>
<option value="todo">للتنفيذ</option>
<option value="in-progress">قيد التنفيذ</option>
<option value="done">مكتملة</option>
</select>
<select
value={searchParams.get("priority") || "all"}
onChange={(e) => updateFilter("priority", e.target.value)}
className="border rounded-lg px-3 py-2"
>
<option value="all">كل الأولويات</option>
<option value="low">منخفضة</option>
<option value="medium">متوسطة</option>
<option value="high">عالية</option>
</select>
</div>
);
}الخطوة 12: بناء صفحات الإنشاء والتعديل
أنشئ src/app/tasks/new/page.tsx:
import Link from "next/link";
import { TaskForm } from "../task-form";
export default function NewTaskPage() {
return (
<div className="max-w-5xl mx-auto px-4 py-8">
<div className="mb-8">
<Link href="/tasks" className="text-blue-600 hover:underline">
← العودة للمهام
</Link>
<h1 className="text-3xl font-bold mt-4">إنشاء مهمة جديدة</h1>
</div>
<TaskForm />
</div>
);
}أنشئ src/app/tasks/[id]/edit/page.tsx:
import Link from "next/link";
import { notFound } from "next/navigation";
import { getTaskById } from "@/lib/actions/task-queries";
import { TaskForm } from "../../task-form";
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function EditTaskPage({ params }: PageProps) {
const { id } = await params;
const task = await getTaskById(id);
if (!task) notFound();
return (
<div className="max-w-5xl mx-auto px-4 py-8">
<div className="mb-8">
<Link href="/tasks" className="text-blue-600 hover:underline">
← العودة للمهام
</Link>
<h1 className="text-3xl font-bold mt-4">تعديل المهمة</h1>
</div>
<TaskForm task={task} />
</div>
);
}الخطوة 13: أنماط متقدمة
خطوط التجميع (Aggregation Pipelines)
إطار التجميع في MongoDB قوي للتحليلات. إليك كيفية الحصول على معدلات إكمال المهام حسب الأولوية:
export async function getCompletionRatesByPriority() {
await connectDB();
const results = await Task.aggregate([
{
$group: {
_id: "$priority",
total: { $sum: 1 },
completed: {
$sum: { $cond: [{ $eq: ["$status", "done"] }, 1, 0] },
},
},
},
{
$project: {
priority: "$_id",
total: 1,
completed: 1,
rate: {
$round: [{ $multiply: [{ $divide: ["$completed", "$total"] }, 100] }, 1],
},
},
},
{ $sort: { rate: -1 } },
]);
return results;
}البرمجيات الوسيطة لـ Mongoose (Hooks)
أضف خطافات pre/post للعمليات الشائعة:
taskSchema.pre("save", function (next) {
if (this.isModified("status") && this.status === "done") {
this.set("completedAt", new Date());
}
next();
});الحقول الافتراضية (Virtual Fields)
أضف حقول محسوبة دون تخزينها:
taskSchema.virtual("isOverdue").get(function () {
if (!this.dueDate || this.status === "done") return false;
return new Date() > this.dueDate;
});
taskSchema.set("toJSON", { virtuals: true });أفضل ممارسات الإنتاج
1. إدارة حوض الاتصالات
أداة الاتصال التي بنيناها تتعامل مع التجميع، لكن اضبطها للإنتاج:
const opts = {
bufferCommands: false,
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
};2. إدارة الفهارس
تحقق دائمًا من استخدام الفهارس:
db.tasks.find({ status: "todo" }).explain("executionStats")3. معالجة الأخطاء
غلّف عمليات قاعدة البيانات بمعالجة أخطاء مناسبة:
export async function createTask(/* ... */): Promise<ActionState> {
try {
await connectDB();
// ... العملية
} catch (error) {
if (error instanceof mongoose.Error.ValidationError) {
return { success: false, message: "بيانات غير صالحة" };
}
return { success: false, message: "حدث خطأ غير متوقع" };
}
}4. النشر على Vercel
MongoDB Atlas يعمل بشكل ممتاز مع دوال Vercel الخادمية:
npm i -g vercel
vercel
vercel env add MONGODB_URIاستكشاف الأخطاء
"MongoServerError: bad auth"
بيانات اعتماد سلسلة الاتصال خاطئة. تحقق من اسم المستخدم وكلمة المرور في Atlas تحت Database Access.
"MongooseServerSelectionError: connection timed out"
عنوان IP غير مُدرج في القائمة البيضاء. اذهب إلى Atlas Network Access وأضف عنوان IP الحالي.
"OverwriteModelError: Cannot overwrite model"
يحدث أثناء إعادة التحميل الساخن. نمط mongoose.models.Task || في ملف النموذج يمنع هذا.
"buffering timed out after 10000ms"
فشل الاتصال بصمت. تأكد من تعيين MONGODB_URI وأن المجموعة تعمل.
الخطوات التالية
الآن بعد أن لديك تطبيق MongoDB + Next.js يعمل، استكشف هذه التحسينات:
- المصادقة — أضف حسابات المستخدمين مع NextAuth.js
- التحديثات الفورية — استخدم MongoDB Change Streams مع Server-Sent Events
- مرفقات الملفات — خزّن الملفات في MongoDB GridFS أو UploadThing
- البحث النصي المتقدم — ترقية إلى MongoDB Atlas Search للمطابقة الضبابية
- التخزين المؤقت — أضف تخزين Redis المؤقت
الخلاصة
لقد بنيت تطبيق CRUD متكامل مع MongoDB Atlas و Mongoose و Next.js 15 App Router. يتضمن التطبيق مخططات آمنة الأنواع وServer Actions للتعديلات والتحقق بـ Zod والبحث النصي الكامل والتصفية والتقسيم إلى صفحات وإدارة اتصالات جاهزة للإنتاج.
نموذج المستندات المرن في MongoDB يجعله مثاليًا للنماذج الأولية السريعة والتطبيقات ذات المخططات المتطورة. مع طبقة التحقق في Mongoose وServer Actions في Next.js، تحصل على تجربة تطوير منتجة وآمنة الأنواع مع حد أدنى من الشيفرة المتكررة.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء تطبيق متكامل باستخدام Prisma ORM و Next.js 15 App Router
تعلم كيفية بناء تطبيق متكامل باستخدام Prisma ORM و Next.js 15 App Router و PostgreSQL. يغطي هذا الدليل نمذجة المخطط والترحيلات و Server Actions وعمليات CRUD والعلاقات والنشر في بيئة الإنتاج.

بناء تطبيق متكامل باستخدام 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. يغطي هذا الدليل التطبيقي برنامج التشغيل بدون خادم، تفريع قواعد البيانات لعمليات النشر التجريبية، تجميع الاتصالات، وأنماط الإنتاج الجاهزة.