Kysely: منشئ استعلامات SQL آمن الأنواع مع Next.js و PostgreSQL 2026

Kysely هو منشئ استعلامات SQL مصمم خصيصًا لـ TypeScript، يمنحك أمان ORM مع شفافية SQL الخام. على عكس Prisma أو Drizzle، لا يخفي Kysely أبدًا ما يتم تنفيذه فعليًا — كل استعلام يُقرأ مثل SQL، وكل عمود مكتوب بأنواع كاملة. في هذا الدرس، ستبني تطبيق Next.js 15 جاهزًا للإنتاج باستخدام Kysely مع PostgreSQL، يشمل عمليات الهجرة والاستعلامات المتقدمة والتكامل مع Server Actions.
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js الإصدار 20 أو أحدث
- نسخة من PostgreSQL الإصدار 15 أو أحدث (محلية أو سحابية — Neon أو Supabase أو Railway تعمل جميعها)
- معرفة أساسية بـ TypeScript و SQL
- محرر أكواد (يُنصح بـ VS Code)
- معرفة بأساسيات Next.js App Router
ما الذي ستبنيه
واجهة برمجة تطبيقات كاملة لإدارة المهام لفريق صغير، تشمل:
- تعريفات مخطط آمنة الأنواع تُولَّد تلقائيًا من PostgreSQL
- عمليات CRUD عبر Server Actions في Next.js 15
- وصلات معقدة وتجميعات وعمليات معاملات
- إدارة هجرات المخطط بواسطة migrator الخاص بـ Kysely
- IntelliSense كامل لكل عمود، في كل استعلام
في النهاية، ستحصل على أساس قوي يتوسع من جدول واحد إلى عشرات الجداول، مع أمان أنواع شامل من البداية للنهاية.
الخطوة 1: إعداد المشروع
أنشئ مشروع Next.js 15 جديدًا مع TypeScript و Tailwind CSS مُهيَّأَين مسبقًا.
npx create-next-app@latest kysely-tasks --typescript --tailwind --app --src-dir --eslint
cd kysely-tasksتحقق من أن المشروع يعمل بشكل سليم قبل المتابعة.
npm run devالخطوة 2: تثبيت Kysely وتبعيات PostgreSQL
ثبّت Kysely مع مشغّل PostgreSQL الرسمي ومُولِّد أكواد يفحص مخطط قاعدة بياناتك.
npm install kysely pg
npm install -D @types/pg kysely-codegen tsxتقرأ حزمة kysely-codegen مخطط PostgreSQL الحي وتُصدر أنواع TypeScript تلقائيًا — هذا هو السحر الذي يدعم IntelliSense الكامل دون كتابة الأنواع يدويًا.
الخطوة 3: إعداد متغيرات البيئة
أنشئ ملف .env.local في جذر المشروع.
DATABASE_URL=postgres://user:password@localhost:5432/kysely_tasksإذا لم تكن لديك نسخة PostgreSQL محلية، قم بتشغيل واحدة بسرعة باستخدام Docker.
docker run --name kysely-pg -e POSTGRES_PASSWORD=password -e POSTGRES_DB=kysely_tasks -p 5432:5432 -d postgres:16الخطوة 4: إنشاء أول عملية هجرة
يأتي Kysely مع migrator مدمج. أنشئ مجلد migrations وسكريبت لتشغيل عمليات الهجرة المعلقة.
أنشئ الملف src/db/migrations/2026_01_01_create_tasks.ts.
import { Kysely, sql } from "kysely";
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable("users")
.addColumn("id", "uuid", (col) =>
col.primaryKey().defaultTo(sql`gen_random_uuid()`)
)
.addColumn("email", "varchar(255)", (col) => col.notNull().unique())
.addColumn("name", "varchar(255)", (col) => col.notNull())
.addColumn("created_at", "timestamptz", (col) =>
col.notNull().defaultTo(sql`now()`)
)
.execute();
await db.schema
.createTable("tasks")
.addColumn("id", "uuid", (col) =>
col.primaryKey().defaultTo(sql`gen_random_uuid()`)
)
.addColumn("title", "varchar(500)", (col) => col.notNull())
.addColumn("description", "text")
.addColumn("status", "varchar(20)", (col) =>
col.notNull().defaultTo("todo")
)
.addColumn("assignee_id", "uuid", (col) =>
col.references("users.id").onDelete("set null")
)
.addColumn("due_date", "date")
.addColumn("created_at", "timestamptz", (col) =>
col.notNull().defaultTo(sql`now()`)
)
.execute();
await db.schema
.createIndex("tasks_assignee_idx")
.on("tasks")
.column("assignee_id")
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable("tasks").execute();
await db.schema.dropTable("users").execute();
}لاحظ كيف أن كل نوع عمود وكل قيمة افتراضية وكل قيد يتم التعبير عنه بـ TypeScript خالص مع إكمال تلقائي كامل.
الخطوة 5: ربط الـ Migrator
أنشئ src/db/migrate.ts — وهو السكريبت الذي تشغّله لتطبيق عمليات الهجرة.
import { promises as fs } from "node:fs";
import path from "node:path";
import { Kysely, Migrator, FileMigrationProvider, PostgresDialect } from "kysely";
import { Pool } from "pg";
async function migrate() {
const db = new Kysely<any>({
dialect: new PostgresDialect({
pool: new Pool({ connectionString: process.env.DATABASE_URL }),
}),
});
const migrator = new Migrator({
db,
provider: new FileMigrationProvider({
fs,
path,
migrationFolder: path.join(process.cwd(), "src/db/migrations"),
}),
});
const { error, results } = await migrator.migrateToLatest();
results?.forEach((r) => {
if (r.status === "Success") {
console.log(`تم تطبيق الهجرة "${r.migrationName}"`);
} else if (r.status === "Error") {
console.error(`فشل تطبيق "${r.migrationName}"`);
}
});
if (error) {
console.error("فشلت عملية الهجرة:", error);
process.exit(1);
}
await db.destroy();
}
migrate();أضف سكريبت إلى package.json.
{
"scripts": {
"db:migrate": "tsx --env-file=.env.local src/db/migrate.ts",
"db:codegen": "kysely-codegen --out-file src/db/types.ts --camel-case"
}
}طبّق عمليات الهجرة الخاصة بك.
npm run db:migrateالخطوة 6: توليد تعريفات الأنواع
شغّل الآن مولّد الأكواد. سيفحص قاعدة بياناتك الحية ويُصدر ملف types.ts يمثّل كل جدول.
DATABASE_URL=$DATABASE_URL npm run db:codegenافتح src/db/types.ts — سترى واجهة DB كاملة الأنواع يستخدمها Kysely لفرض الأمان على كل استعلام.
export interface DB {
users: Users;
tasks: Tasks;
}
export interface Users {
id: Generated<string>;
email: string;
name: string;
createdAt: Generated<Date>;
}
export interface Tasks {
id: Generated<string>;
title: string;
description: string | null;
status: Generated<string>;
assigneeId: string | null;
dueDate: Date | null;
createdAt: Generated<Date>;
}الخطوة 7: إنشاء عميل قاعدة البيانات
أنشئ src/db/client.ts لتجسيد نسخة Kysely واحدة للتطبيق.
import { Kysely, PostgresDialect, CamelCasePlugin } from "kysely";
import { Pool } from "pg";
import type { DB } from "./types";
const globalForDb = globalThis as unknown as {
db: Kysely<DB> | undefined;
};
export const db =
globalForDb.db ??
new Kysely<DB>({
dialect: new PostgresDialect({
pool: new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
}),
}),
plugins: [new CamelCasePlugin()],
});
if (process.env.NODE_ENV !== "production") {
globalForDb.db = db;
}يحوّل CamelCasePlugin تلقائيًا الأعمدة من snake_case في PostgreSQL إلى camelCase في TypeScript. يمنع التخزين المؤقت العام إعادة تحميل Next.js الفوري من إنشاء حمامات اتصالات مكررة في وضع التطوير.
الخطوة 8: بناء Server Actions لـ CRUD
أنشئ src/app/actions/tasks.ts — وحدة Server Actions تعمل بقوة Kysely.
"use server";
import { revalidatePath } from "next/cache";
import { db } from "@/db/client";
export async function createTask(formData: FormData) {
const title = formData.get("title") as string;
const assigneeId = (formData.get("assigneeId") as string) || null;
const task = await db
.insertInto("tasks")
.values({
title,
assigneeId,
})
.returningAll()
.executeTakeFirstOrThrow();
revalidatePath("/");
return task;
}
export async function listTasks() {
return db
.selectFrom("tasks")
.leftJoin("users", "users.id", "tasks.assigneeId")
.select([
"tasks.id",
"tasks.title",
"tasks.description",
"tasks.status",
"tasks.dueDate",
"users.name as assigneeName",
])
.orderBy("tasks.createdAt", "desc")
.execute();
}
export async function updateTaskStatus(id: string, status: string) {
await db
.updateTable("tasks")
.set({ status })
.where("id", "=", id)
.execute();
revalidatePath("/");
}
export async function deleteTask(id: string) {
await db.deleteFrom("tasks").where("id", "=", id).execute();
revalidatePath("/");
}لاحظ IntelliSense — جرّب إعادة تسمية tasks.title إلى tasks.titlex وراقب TypeScript يرفض البناء فورًا. لا يوجد عبء وقت تشغيل لأن كل شيء يُترجم إلى SQL خالص.
الخطوة 9: بناء واجهة المستخدم
أنشئ صفحة قائمة مهام بسيطة في src/app/page.tsx.
import { listTasks, createTask, updateTaskStatus } from "./actions/tasks";
export default async function HomePage() {
const tasks = await listTasks();
return (
<main className="max-w-3xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">مهام الفريق</h1>
<form action={createTask} className="flex gap-2 mb-8">
<input
type="text"
name="title"
placeholder="ما الذي يجب فعله؟"
className="flex-1 border rounded px-3 py-2"
required
/>
<button type="submit" className="bg-black text-white px-4 py-2 rounded">
إضافة
</button>
</form>
<ul className="space-y-2">
{tasks.map((task) => (
<li
key={task.id}
className="border rounded p-4 flex items-center justify-between"
>
<div>
<div className="font-medium">{task.title}</div>
{task.assigneeName && (
<div className="text-sm text-gray-600">
مُسنَدة إلى {task.assigneeName}
</div>
)}
</div>
<form
action={async () => {
"use server";
await updateTaskStatus(
task.id,
task.status === "done" ? "todo" : "done"
);
}}
>
<button className="text-sm text-blue-600">
{task.status === "done" ? "إعادة فتح" : "إنجاز"}
</button>
</form>
</li>
))}
</ul>
</main>
);
}الخطوة 10: إتقان الاستعلامات المتقدمة
يتألق Kysely حقًا في استعلامات SQL المتقدمة. إليك أنماطًا ستستخدمها باستمرار.
التجميعات والتجميع حسب المجموعة
import { sql } from "kysely";
const stats = await db
.selectFrom("tasks")
.select([
"status",
db.fn.count<number>("id").as("total"),
sql<number>`COUNT(*) FILTER (WHERE due_date < now())`.as("overdue"),
])
.groupBy("status")
.execute();Common Table Expressions
const result = await db
.with("recent_tasks", (cte) =>
cte
.selectFrom("tasks")
.selectAll()
.where("createdAt", ">", new Date(Date.now() - 7 * 86400000))
)
.selectFrom("recent_tasks")
.selectAll()
.where("status", "=", "todo")
.execute();المعاملات (Transactions)
await db.transaction().execute(async (trx) => {
const user = await trx
.insertInto("users")
.values({ email: "alice@example.com", name: "Alice" })
.returning("id")
.executeTakeFirstOrThrow();
await trx
.insertInto("tasks")
.values({
title: "أهلًا بك معنا",
assigneeId: user.id,
})
.execute();
});إذا رمى أي شيء داخل دالة الاستدعاء استثناءً، يقوم Kysely تلقائيًا بإلغاء المعاملة بالكامل.
الخطوة 11: مخارج SQL الخام الآمنة الأنواع
عندما تحتاج إلى ميزات PostgreSQL لم يتمكن Kysely من تجريدها بعد، عُد إلى SQL الخام المكتوب الأنواع.
import { sql } from "kysely";
const fuzzyMatches = await db
.selectFrom("tasks")
.select(["id", "title"])
.where(sql<boolean>`title % ${"deploy"}`)
.orderBy(sql`similarity(title, ${"deploy"})`, "desc")
.limit(10)
.execute();يحافظ القالب sql على استدلال الأنواع من البداية للنهاية مع السماح لك باستخدام أي امتداد من امتدادات PostgreSQL تحتاجه.
الخطوة 12: اختبار التنفيذ الخاص بك
شغّل قاعدة بيانات مؤقتة للاختبارات باستخدام pg-mem أو حاوية Docker، ثم نفّذ اختبارات التكامل باستخدام Vitest.
import { describe, it, expect, beforeEach } from "vitest";
import { db } from "@/db/client";
describe("tasks", () => {
beforeEach(async () => {
await db.deleteFrom("tasks").execute();
});
it("ينشئ ويقرأ مهمة", async () => {
await db
.insertInto("tasks")
.values({ title: "إطلاق" })
.execute();
const found = await db
.selectFrom("tasks")
.selectAll()
.where("title", "=", "إطلاق")
.executeTakeFirst();
expect(found?.title).toBe("إطلاق");
});
});استكشاف الأخطاء وإصلاحها
المشكلات الشائعة وكيفية حلها.
- استنفاد بحيرة الاتصال: قلّل من قيمة
maxأو أغلق الوصول إلى قاعدة البيانات في استدعاءات serverless طويلة الأمد عبرdb.destroy(). - مخرجات codegen فارغة: تأكّد من إمكانية الوصول إلى
DATABASE_URLمن الصدفة الخاصة بك ومن وجود جدول واحد على الأقل معرَّف من المستخدم في قاعدة البيانات. - عدم تطابق CamelCase: إذا نسيت
CamelCasePlugin، تُعيد الاستعلامات مفاتيحsnake_caseبينما تتوقع الأنواع المُولَّدة camelCase. اقرن دائمًا الإضافة مع الراية--camel-caseفي codegen. - عدم اكتشاف الهجرات: يجب أن تتبع أسماء الملفات الترتيب المعجمي. استخدم بادئة تاريخ مثل
2026_01_01_ليتم فرزها بشكل صحيح.
نصائح الأداء
- أضف دائمًا استدعاءات
select()صريحة بدلاً منselectAll()في كود الإنتاج لتجنّب جلب أعمدة غير ضرورية. - استخدم
executeTakeFirst()بدلاً منexecute()عند الحاجة إلى صف واحد — هذا يتجنب إنشاء مصفوفة. - غلّف الكتابات متعددة الخطوات داخل معاملات لمنع الفشل الجزئي.
- ادمج Kysely مع
pgbouncerأو تجميع اتصالات Neon لعمليات النشر serverless.
الخطوات التالية
الآن وقد أصبحت لديك طبقة قاعدة بيانات آمنة الأنواع، فكّر في توسيعها بـ:
- نسخ متماثلة للقراءة باستخدام عدة نسخ من Kysely
- سياسات أمان على مستوى الصف في PostgreSQL مع
withSchema()الخاصة بـ Kysely - مهام في الخلفية تستدعي طبقة الاستعلامات نفسها من العامل
- إقران Kysely مع درس Drizzle ORM لمقارنة الأساليب
- النشر إلى الإنتاج عبر دليل Coolify
الخاتمة
يحقق Kysely توازنًا فريدًا — يمنحك القوة الكاملة وشفافية SQL دون التنازل أبدًا عن أمان TypeScript. لقد كتبت استعلامات حقيقية، واستخدمت وصلات حقيقية، وأطلقت عمليات هجرة حقيقية، كل ذلك دون أن يخفي ORM السلوك خلف طبقة من السحر. بالنسبة للفرق التي تفكّر بالفعل بلغة SQL، يُعدّ Kysely في الغالب أنظف طريق نحو تطبيق Next.js قابل للصيانة وآمن الأنواع.
كلما وجدت نفسك تكافح مع تجريدات ORM أو تتساءل عن الاستعلام الذي تم تنفيذه فعليًا، تذكّر أن Kysely يتيح لك قراءة وكتابة SQL بالضبط كما قصدت — مع المُترجم يحرس ظهرك.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

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

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

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