Neon Serverless Postgres مع Next.js App Router: بناء تطبيق كامل مع تفريع قواعد البيانات

Postgres بدون خادم يتوسع إلى الصفر. Neon هي منصة Postgres مُدارة بالكامل مصممة لتطبيقات الحوسبة بدون خادم والحافة الحديثة. في هذا الدليل التطبيقي، ستبني مدير إشارات مرجعية كامل باستخدام Next.js 15 وبرنامج تشغيل Neon بدون خادم وتفريع قواعد البيانات للنشر التجريبي الآمن.
ما ستتعلمه
بنهاية هذا الدليل التطبيقي، ستتمكن من:
- إعداد قاعدة بيانات Neon Postgres مع تجميع الاتصالات بدون خادم
- استخدام برنامج تشغيل Neon بدون خادم (
@neondatabase/serverless) للاستعلامات عبر HTTP - بناء مدير إشارات مرجعية كامل مع Next.js 15 App Router
- تنفيذ Server Actions لعمليات قاعدة البيانات
- إنشاء فروع قواعد بيانات لعمليات النشر التجريبية
- تكوين تجميع الاتصالات لأداء الإنتاج
- النشر مع إدارة متغيرات البيئة بشكل صحيح
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 20+ مثبت (
node --version) - خبرة في TypeScript (الأنواع، async/await)
- معرفة بـ Next.js (App Router، Server Components، Server Actions)
- حساب Neon — الطبقة المجانية على neon.tech (لا حاجة لبطاقة ائتمان)
- محرر أكواد — VS Code أو Cursor مُوصى بهما
لماذا Neon؟
PostgreSQL هو المعيار الذهبي لقواعد البيانات العلائقية، لكن Postgres التقليدي يتطلب خوادم تعمل باستمرار. يغير Neon هذا ببنية بدون خادم مصممة من الصفر:
| الميزة | Postgres التقليدي | Neon بدون خادم |
|---|---|---|
| التوسع | توسع رأسي يدوي | يتوسع تلقائياً إلى الصفر ويرتفع عند الطلب |
| البدء البارد | لا ينطبق (يعمل دائماً) | أقل من 500 مللي ثانية للاستيقاظ |
| التفريع | pg_dump + استعادة يدوية | فروع فورية بنسخ عند الكتابة |
| التكلفة | تدفع لوقت الخمول | تدفع فقط للحوسبة والتخزين المستخدم |
| نموذج الاتصال | اتصالات TCP مستمرة | استعلامات HTTP + تجميع WebSocket |
| توافق الحافة | يتطلب TCP (غير ملائم للحافة) | يعمل في Vercel Edge وCloudflare Workers |
الميزة القاتلة هي تفريع قواعد البيانات — يمكنك إنشاء نسخة فورية من قاعدة بيانات الإنتاج لكل طلب سحب، تماماً مثل فروع Git لبياناتك.
الخطوة 1: إنشاء مشروع Neon
- سجّل في neon.tech وأنشئ مشروعاً جديداً
- اختر منطقتك (اختر واحدة قريبة من هدف النشر)
- سمّ مشروعك
bookmarks-app - ينشئ Neon فرعاً افتراضياً
mainمع قاعدة بياناتneondb
بعد الإنشاء، سترى سلسلة الاتصال الخاصة بك. تبدو هكذا:
postgres://username:password@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require
احفظ هذا — ستحتاجه في الخطوة التالية. يوفر Neon سلسلتي اتصال:
- اتصال مباشر — للهجرات والمهام الإدارية
- اتصال مُجمَّع — لاستعلامات التطبيق (يستخدم تجميع الاتصالات عبر PgBouncer)
الاتصال المُجمَّع يضيف -pooler لاسم المضيف:
postgres://username:password@ep-cool-name-123456-pooler.us-east-2.aws.neon.tech/neondb?sslmode=require
الخطوة 2: بناء هيكل مشروع Next.js
أنشئ مشروع Next.js 15 جديد مع TypeScript:
npx create-next-app@latest bookmarks-app --typescript --tailwind --eslint --app --src-dir --use-npm
cd bookmarks-appثبّت برنامج تشغيل Neon بدون خادم وDrizzle ORM للاستعلامات الآمنة من حيث النوع:
npm install @neondatabase/serverless drizzle-orm
npm install -D drizzle-kit dotenvلماذا برنامج تشغيل Neon بدون خادم؟
حزمة @neondatabase/serverless تستبدل برنامج التشغيل التقليدي pg. تتواصل عبر HTTP وWebSockets بدلاً من TCP، مما يعني:
- تعمل في بيئات تشغيل الحافة (Vercel Edge Functions، Cloudflare Workers)
- لا عبء اتصال مستمر — كل استعلام هو طلب HTTP عديم الحالة
- تجميع اتصالات تلقائي عبر وكيل Neon
- عبء أقل من مللي ثانية مقارنة باتصالات TCP التقليدية
الخطوة 3: تكوين متغيرات البيئة
أنشئ ملف .env.local في جذر مشروعك:
# قاعدة بيانات Neon
DATABASE_URL="postgres://username:password@ep-cool-name-123456-pooler.us-east-2.aws.neon.tech/neondb?sslmode=require"
DIRECT_DATABASE_URL="postgres://username:password@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require"استبدل القيم بسلاسل اتصال Neon الفعلية. يستخدم DATABASE_URL الاتصال المُجمَّع لاستعلامات التطبيق، بينما يستخدم DIRECT_DATABASE_URL الاتصال المباشر للهجرات.
الخطوة 4: تعريف مخطط قاعدة البيانات
أنشئ ملف المخطط في src/db/schema.ts:
import { pgTable, serial, text, varchar, timestamp, boolean, integer } from "drizzle-orm/pg-core";
export const bookmarks = pgTable("bookmarks", {
id: serial("id").primaryKey(),
title: varchar("title", { length: 255 }).notNull(),
url: text("url").notNull(),
description: text("description"),
tags: text("tags").array(),
isFavorite: boolean("is_favorite").default(false).notNull(),
clickCount: integer("click_count").default(0).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
export type Bookmark = typeof bookmarks.$inferSelect;
export type NewBookmark = typeof bookmarks.$inferInsert;يعرّف هذا المخطط جدول bookmarks مع:
idيتزايد تلقائياً كمفتاح أساسيtitleوurlكحقول مطلوبةdescriptionاختياري ومصفوفةtagsisFavoriteمنطقي للتصفية السريعةclickCountعدد صحيح لتتبع الشعبية- طوابع زمنية تلقائية لـ
createdAtوupdatedAt
الخطوة 5: إعداد Drizzle مع برنامج تشغيل Neon
أنشئ اتصال قاعدة البيانات في src/db/index.ts:
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";
if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL environment variable is not set");
}
const sql = neon(process.env.DATABASE_URL);
export const db = drizzle(sql, { schema });هذه هي نقطة التكامل الرئيسية. بدلاً من استخدام pg Pool التقليدي، تستخدم neon() لإنشاء منفذ SQL قائم على HTTP، ثم تمرره لمحول neon-http الخاص بـ Drizzle.
فهم تدفق الاتصال
تطبيقك → طلب HTTP → وكيل Neon (المُجمِّع) → Neon Postgres
كل استعلام هو طلب HTTP عديم الحالة. لا يوجد اتصال لإدارته، ولا مجمع لتكوينه، ولا حدود اتصال للقلق بشأنها. يتعامل وكيل Neon مع تجميع الاتصالات من جانبهم.
الخطوة 6: تكوين Drizzle Kit للهجرات
أنشئ drizzle.config.ts في جذر المشروع:
import { defineConfig } from "drizzle-kit";
import dotenv from "dotenv";
dotenv.config({ path: ".env.local" });
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DIRECT_DATABASE_URL!,
},
});لاحظ أننا نستخدم DIRECT_DATABASE_URL (وليس المُجمَّع) للهجرات. تتطلب الهجرات عبارات DDL التي تعمل بشكل أفضل عبر الاتصالات المباشرة.
أضف سكريبتات الهجرة إلى package.json:
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio"
}
}الآن ادفع المخطط إلى قاعدة بيانات Neon:
npm run db:pushيجب أن ترى مخرجات تؤكد إنشاء جدول bookmarks. يمكنك التحقق بفتح Drizzle Studio:
npm run db:studioهذا يفتح متصفح قاعدة بيانات مرئي في https://local.drizzle.studio حيث يمكنك فحص الجداول والبيانات.
الخطوة 7: بناء Server Actions لعمليات CRUD
أنشئ src/app/actions.ts لجميع عمليات قاعدة البيانات:
"use server";
import { db } from "@/db";
import { bookmarks } from "@/db/schema";
import { eq, desc, sql, ilike, or } from "drizzle-orm";
import { revalidatePath } from "next/cache";
export async function getBookmarks(search?: string) {
if (search) {
return db
.select()
.from(bookmarks)
.where(
or(
ilike(bookmarks.title, `%${search}%`),
ilike(bookmarks.url, `%${search}%`),
ilike(bookmarks.description, `%${search}%`)
)
)
.orderBy(desc(bookmarks.createdAt));
}
return db
.select()
.from(bookmarks)
.orderBy(desc(bookmarks.createdAt));
}
export async function createBookmark(formData: FormData) {
const title = formData.get("title") as string;
const url = formData.get("url") as string;
const description = formData.get("description") as string;
const tags = (formData.get("tags") as string)
?.split(",")
.map((t) => t.trim())
.filter(Boolean);
await db.insert(bookmarks).values({
title,
url,
description: description || null,
tags: tags?.length ? tags : null,
});
revalidatePath("/");
}
export async function toggleFavorite(id: number) {
const [bookmark] = await db
.select({ isFavorite: bookmarks.isFavorite })
.from(bookmarks)
.where(eq(bookmarks.id, id));
if (bookmark) {
await db
.update(bookmarks)
.set({ isFavorite: !bookmark.isFavorite })
.where(eq(bookmarks.id, id));
}
revalidatePath("/");
}
export async function incrementClickCount(id: number) {
await db
.update(bookmarks)
.set({
clickCount: sql`${bookmarks.clickCount} + 1`,
})
.where(eq(bookmarks.id, id));
}
export async function deleteBookmark(id: number) {
await db.delete(bookmarks).where(eq(bookmarks.id, id));
revalidatePath("/");
}
export async function getStats() {
const [result] = await db
.select({
total: sql<number>`count(*)`,
favorites: sql<number>`count(*) filter (where ${bookmarks.isFavorite} = true)`,
totalClicks: sql<number>`coalesce(sum(${bookmarks.clickCount}), 0)`,
})
.from(bookmarks);
return result;
}كل Server Action يتواصل مباشرة مع Neon عبر HTTP. لا توجد طبقة مسارات API — Server Actions في Next.js تستدعي قاعدة البيانات مباشرة من الخادم، وبرنامج تشغيل Neon يتعامل مع نقل HTTP.
الخطوة 8: بناء مكونات واجهة المستخدم
مكون بطاقة الإشارة المرجعية
أنشئ src/components/bookmark-card.tsx:
"use client";
import { Bookmark } from "@/db/schema";
import { toggleFavorite, deleteBookmark, incrementClickCount } from "@/app/actions";
export function BookmarkCard({ bookmark }: { bookmark: Bookmark }) {
const handleClick = async () => {
await incrementClickCount(bookmark.id);
};
return (
<div className="border rounded-lg p-4 hover:shadow-md transition-shadow bg-white">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<a
href={bookmark.url}
target="_blank"
rel="noopener noreferrer"
onClick={handleClick}
className="text-lg font-semibold text-blue-600 hover:text-blue-800 hover:underline truncate block"
>
{bookmark.title}
</a>
<p className="text-sm text-gray-500 truncate mt-1">{bookmark.url}</p>
</div>
<div className="flex gap-2 ml-4">
<button
onClick={() => toggleFavorite(bookmark.id)}
className="text-xl hover:scale-110 transition-transform"
title={bookmark.isFavorite ? "إزالة من المفضلة" : "إضافة للمفضلة"}
>
{bookmark.isFavorite ? "\u2605" : "\u2606"}
</button>
<button
onClick={() => deleteBookmark(bookmark.id)}
className="text-red-400 hover:text-red-600 text-sm"
title="حذف الإشارة المرجعية"
>
حذف
</button>
</div>
</div>
{bookmark.description && (
<p className="text-gray-600 mt-2 text-sm line-clamp-2">
{bookmark.description}
</p>
)}
<div className="flex items-center justify-between mt-3">
<div className="flex gap-1 flex-wrap">
{bookmark.tags?.map((tag) => (
<span
key={tag}
className="px-2 py-0.5 bg-gray-100 text-gray-600 rounded-full text-xs"
>
{tag}
</span>
))}
</div>
<span className="text-xs text-gray-400">
{bookmark.clickCount} نقرة
</span>
</div>
</div>
);
}نموذج إضافة إشارة مرجعية
أنشئ src/components/add-bookmark-form.tsx:
"use client";
import { createBookmark } from "@/app/actions";
import { useRef } from "react";
export function AddBookmarkForm() {
const formRef = useRef<HTMLFormElement>(null);
const handleSubmit = async (formData: FormData) => {
await createBookmark(formData);
formRef.current?.reset();
};
return (
<form ref={formRef} action={handleSubmit} className="space-y-4 bg-white p-6 rounded-lg border">
<h2 className="text-lg font-semibold">إضافة إشارة مرجعية جديدة</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1">
العنوان *
</label>
<input
type="text"
id="title"
name="title"
required
className="w-full px-3 py-2 border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="إشارتي المرجعية"
/>
</div>
<div>
<label htmlFor="url" className="block text-sm font-medium text-gray-700 mb-1">
الرابط *
</label>
<input
type="url"
id="url"
name="url"
required
className="w-full px-3 py-2 border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="https://example.com"
/>
</div>
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
الوصف
</label>
<textarea
id="description"
name="description"
rows={2}
className="w-full px-3 py-2 border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="وصف مختصر..."
/>
</div>
<div>
<label htmlFor="tags" className="block text-sm font-medium text-gray-700 mb-1">
الوسوم (مفصولة بفواصل)
</label>
<input
type="text"
id="tags"
name="tags"
className="w-full px-3 py-2 border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="react, tutorial, database"
/>
</div>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
إضافة إشارة مرجعية
</button>
</form>
);
}الخطوة 9: بناء الصفحة الرئيسية
حدّث src/app/page.tsx:
import { getBookmarks, getStats } from "./actions";
import { BookmarkCard } from "@/components/bookmark-card";
import { AddBookmarkForm } from "@/components/add-bookmark-form";
export default async function Home({
searchParams,
}: {
searchParams: Promise<{ search?: string }>;
}) {
const { search } = await searchParams;
const [allBookmarks, stats] = await Promise.all([
getBookmarks(search),
getStats(),
]);
return (
<main className="max-w-4xl mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-2">مدير الإشارات المرجعية</h1>
<p className="text-gray-500 mb-8">
مدعوم بـ Neon Serverless Postgres
</p>
{/* الإحصائيات */}
<div className="grid grid-cols-3 gap-4 mb-8">
<div className="bg-blue-50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-blue-600">{stats.total}</div>
<div className="text-sm text-gray-600">إجمالي الإشارات</div>
</div>
<div className="bg-yellow-50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-yellow-600">{stats.favorites}</div>
<div className="text-sm text-gray-600">المفضلة</div>
</div>
<div className="bg-green-50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-green-600">{stats.totalClicks}</div>
<div className="text-sm text-gray-600">إجمالي النقرات</div>
</div>
</div>
{/* نموذج الإضافة */}
<div className="mb-8">
<AddBookmarkForm />
</div>
{/* البحث */}
<form className="mb-6">
<input
type="text"
name="search"
defaultValue={search}
placeholder="بحث في الإشارات المرجعية..."
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</form>
{/* قائمة الإشارات المرجعية */}
<div className="space-y-4">
{allBookmarks.length === 0 ? (
<div className="text-center py-12 text-gray-400">
<p className="text-lg">لا توجد إشارات مرجعية بعد</p>
<p className="text-sm mt-1">أضف إشارتك المرجعية الأولى أعلاه</p>
</div>
) : (
allBookmarks.map((bookmark) => (
<BookmarkCard key={bookmark.id} bookmark={bookmark} />
))
)}
</div>
</main>
);
}شغّل خادم التطوير للتحقق من أن كل شيء يعمل:
npm run devافتح http://localhost:3000 ويجب أن ترى مدير الإشارات المرجعية. جرّب إضافة بعض الإشارات المرجعية وتمييزها كمفضلة والبحث.
الخطوة 10: تفريع قواعد البيانات لعمليات النشر التجريبية
هنا يتألق Neon حقاً. يُنشئ تفريع قواعد البيانات نسخة فورية قائمة على النسخ عند الكتابة من قاعدة بياناتك — مثالية لعمليات النشر التجريبية واختبار الهجرات أو تجربة البيانات.
كيف يعمل التفريع
يستخدم Neon طبقة تخزين نسخ عند الكتابة مستوحاة من Git:
الفرع الرئيسي (بيانات الإنتاج)
└── preview/feature-xyz (نسخة فورية، تغييرات معزولة)
- إنشاء فوري — يتم إنشاء الفروع في أجزاء من الثانية بغض النظر عن حجم قاعدة البيانات
- لا عبء تخزين — تتشارك الفروع صفحات البيانات حتى التعديل
- عزل كامل — التغييرات على فرع لا تؤثر أبداً على الأصل
- تنظيف تلقائي — يمكن حذف الفروع عند دمج طلب السحب
إنشاء فرع عبر API الخاص بـ Neon
يمكنك أتمتة إنشاء الفروع في خط أنابيب CI/CD. إليك كيفية إنشاء فرع باستخدام Neon API:
// scripts/create-neon-branch.ts
const NEON_API_KEY = process.env.NEON_API_KEY!;
const NEON_PROJECT_ID = process.env.NEON_PROJECT_ID!;
async function createBranch(branchName: string) {
const response = await fetch(
`https://console.neon.tech/api/v2/projects/${NEON_PROJECT_ID}/branches`,
{
method: "POST",
headers: {
Authorization: `Bearer ${NEON_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
branch: {
name: branchName,
parent_id: undefined,
},
endpoints: [
{
type: "read_write",
},
],
}),
}
);
const data = await response.json();
const endpoint = data.endpoints[0];
const host = endpoint.host;
const dbName = "neondb";
const role = data.roles?.[0]?.name || "neondb_owner";
console.log(`تم إنشاء الفرع: ${data.branch.name}`);
console.log(`الاتصال: postgres://${role}@${host}/${dbName}?sslmode=require`);
return data;
}
const prNumber = process.argv[2];
if (prNumber) {
createBranch(`preview/pr-${prNumber}`);
}تكامل GitHub Actions
أضف هذا السير التلقائي لإنشاء فرع Neon تلقائياً لكل طلب سحب:
# .github/workflows/preview-branch.yml
name: Create Preview Database
on:
pull_request:
types: [opened, synchronize]
jobs:
create-branch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create Neon Branch
uses: neondatabase/create-branch-action@v5
id: create-branch
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
api_key: ${{ secrets.NEON_API_KEY }}
branch_name: preview/pr-${{ github.event.pull_request.number }}
- name: Set DATABASE_URL
run: |
echo "Preview database URL: ${{ steps.create-branch.outputs.db_url_with_pooler }}"عند دمج أو إغلاق طلب السحب، أضف مهمة تنظيف:
# .github/workflows/cleanup-branch.yml
name: Cleanup Preview Database
on:
pull_request:
types: [closed]
jobs:
delete-branch:
runs-on: ubuntu-latest
steps:
- name: Delete Neon Branch
uses: neondatabase/delete-branch-action@v3
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
api_key: ${{ secrets.NEON_API_KEY }}
branch: preview/pr-${{ github.event.pull_request.number }}هذا يمنحك بيئات معاينة حقيقية لقاعدة البيانات — كل طلب سحب يحصل على قاعدة بياناته الخاصة مع بيانات الإنتاج، معزولة تماماً عن الإنتاج.
الخطوة 11: تجميع الاتصالات والأداء
فهم أوضاع اتصال Neon
يوفر Neon ثلاثة أوضاع اتصال، كل منها مناسب لحالات استخدام مختلفة:
1. HTTP (برنامج التشغيل بدون خادم)
import { neon } from "@neondatabase/serverless";
const sql = neon(process.env.DATABASE_URL);
const result = await sql`SELECT * FROM bookmarks`;الأفضل لـ: الدوال بدون خادم، بيئات تشغيل الحافة، الاستعلامات الفردية.
2. WebSocket (مُجمَّع)
import { Pool } from "@neondatabase/serverless";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const { rows } = await pool.query("SELECT * FROM bookmarks");الأفضل لـ: عمليات Node.js طويلة التشغيل، استعلامات متعددة لكل طلب.
3. TCP المباشر
import { Client } from "pg";
const client = new Client(process.env.DIRECT_DATABASE_URL);
await client.connect();الأفضل لـ: الهجرات، المهام الإدارية، التطوير المحلي.
اختيار الوضع المناسب لـ Next.js
لتطبيق Next.js App Router:
| السياق | الوضع المُوصى |
|---|---|
| Server Components | HTTP (برنامج تشغيل neon بدون خادم) |
| Server Actions | HTTP (برنامج تشغيل neon بدون خادم) |
| Route Handlers | HTTP أو WebSocket Pool |
| Middleware (الحافة) | HTTP فقط |
| الهجرات | TCP المباشر |
نصائح الأداء
1. استخدم Promise.all للاستعلامات المتوازية:
// سيئ: استعلامات متتالية (بطيء)
const bookmarks = await db.select().from(bookmarksTable);
const stats = await db.select().from(statsTable);
// جيد: استعلامات متوازية (سريع)
const [bookmarks, stats] = await Promise.all([
db.select().from(bookmarksTable),
db.select().from(statsTable),
]);2. اختر فقط الأعمدة المطلوبة:
// سيئ: يجلب كل الأعمدة
const result = await db.select().from(bookmarks);
// جيد: يجلب فقط ما تحتاجه
const result = await db
.select({
id: bookmarks.id,
title: bookmarks.title,
url: bookmarks.url,
})
.from(bookmarks);3. استخدم خيار fetchConnectionCache في Neon:
const sql = neon(process.env.DATABASE_URL, {
fetchConnectionCache: true,
});هذا يعيد استخدام اتصال fetch الأساسي بين الاستعلامات في نفس الطلب، مما يقلل الكمون بـ 10-20 مللي ثانية لكل استعلام.
الخطوة 12: إضافة البحث النصي الكامل مع Postgres
بما أننا نستخدم Postgres حقيقي، نحصل على بحث نصي كامل مدمج — لا حاجة لخدمة بحث خارجية:
أنشئ هجرة لإضافة فهرس GIN للبحث النصي الكامل:
-- drizzle/0001_add_search_index.sql
CREATE INDEX idx_bookmarks_search ON bookmarks
USING GIN (to_tsvector('english', coalesce(title, '') || ' ' || coalesce(description, '')));حدّث إجراء البحث لاستخدام البحث النصي الكامل في Postgres:
export async function searchBookmarks(query: string) {
const results = await db.execute(
sql`SELECT *, ts_rank(
to_tsvector('english', coalesce(title, '') || ' ' || coalesce(description, '')),
plainto_tsquery('english', ${query})
) as rank
FROM bookmarks
WHERE to_tsvector('english', coalesce(title, '') || ' ' || coalesce(description, ''))
@@ plainto_tsquery('english', ${query})
ORDER BY rank DESC`
);
return results.rows;
}هذا يمنحك بحثاً مصنفاً حسب الصلة مدعوماً بـ Postgres — لا حاجة لـ Algolia أو Meilisearch أو Elasticsearch.
استكشاف الأخطاء وإصلاحها
المشاكل الشائعة
"Connection terminated unexpectedly"
يحدث هذا عادة عند استخدام الاتصال المباشر في بيئة بدون خادم. انتقل إلى رابط الاتصال المُجمَّع (مع -pooler في اسم المضيف).
"Too many connections"
إذا رأيت أخطاء حد الاتصال، تأكد من استخدام برنامج تشغيل HTTP (neon()) بدلاً من فئة Pool. لا يحتفظ برنامج تشغيل HTTP باتصالات مفتوحة.
"Endpoint is suspended"
الطبقة المجانية في Neon توقف نقاط النهاية بعد 5 دقائق من عدم النشاط. الاستعلام الأول بعد التوقف يستغرق 300-500 مللي ثانية للاستيقاظ. للإنتاج، قم بالترقية إلى خطة مدفوعة مع نقاط نهاية تعمل دائماً.
فشل الهجرات
استخدم دائماً DIRECT_DATABASE_URL لتشغيل الهجرات. الاتصال المُجمَّع عبر PgBouncer لا يدعم عبارات DDL بشكل جيد.
الخطوات التالية
الآن بعد أن لديك تطبيق عامل مدعوم بـ Neon، إليك طرق لتوسيعه:
- أضف المصادقة مع Auth.js أو Better Auth لجعل الإشارات المرجعية لكل مستخدم
- نفّذ أمان على مستوى الصف باستخدام سياسات Postgres RLS
- أعد التوسع التلقائي لـ Neon للتعامل مع ارتفاعات حركة المرور تلقائياً
- أنشئ REST API مع Route Handlers للتكاملات الخارجية
- أضف تحديثات فورية باستخدام النسخ المنطقي لـ Neon مع WebSockets
- ادمج مع Vercel لعمليات النشر التجريبية التلقائية مع فروع قواعد البيانات
الخلاصة
في هذا الدليل التطبيقي، بنيت مدير إشارات مرجعية كامل مدعوم بـ Neon Serverless Postgres وNext.js 15 App Router. تعلّمت كيفية:
- إعداد Neon مع برنامج تشغيل HTTP بدون خادم لاستعلامات متوافقة مع الحافة
- بناء عمليات قاعدة بيانات آمنة من حيث النوع مع Drizzle ORM
- تنفيذ عمليات CRUD باستخدام Next.js Server Actions
- إنشاء فروع قواعد بيانات لبيئات معاينة معزولة
- تحسين الأداء مع تجميع الاتصالات والاستعلامات المتوازية
- إضافة بحث نصي كامل في Postgres بدون اعتماديات خارجية
يجلب Neon قوة PostgreSQL إلى عالم الحوسبة بدون خادم — التوسع التلقائي والتفريع الفوري وتسعير الدفع حسب الاستخدام يجعلونه خياراً ممتازاً لتطبيقات full-stack الحديثة. مع Next.js App Router وDrizzle ORM، تحصل على تجربة تطوير منتجة وجاهزة للإنتاج في آن واحد.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

البحث النصي الكامل في 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.

Zustand + Next.js App Router: إدارة حالة React الحديثة من الصفر إلى الإنتاج
أتقن إدارة حالة React الحديثة مع Zustand و Next.js 15 App Router. يغطي هذا الدليل العملي إنشاء المتاجر والوسائط والاستمرارية والترطيب من جانب الخادم وأنماط التطبيقات القابلة للتوسع.