بناء واجهة GraphQL آمنة الأنواع مع Next.js App Router و Yoga و Pothos

GraphQL مع استدلال TypeScript الكامل — بدون توليد أكواد. Pothos هو منشئ مخططات يمنحك الإكمال التلقائي وأمان الأنواع عبر واجهة GraphQL API بالكامل، بينما يوفر GraphQL Yoga خادمًا خفيفًا ومتوافقًا مع المواصفات. في هذا الدليل، ستجمع بينهما مع Next.js 15 App Router لبناء واجهة API لإدارة الإشارات المرجعية جاهزة للإنتاج.
ما ستتعلمه
بنهاية هذا الدليل، ستتمكن من:
- إعداد GraphQL Yoga كمعالج مسار داخل Next.js 15 App Router
- تعريف مخطط GraphQL بأسلوب code-first مع Pothos — بدون ملفات SDL وبدون codegen
- بناء استعلامات وتحولات مع إكمال تلقائي كامل في TypeScript
- إضافة وسيط مصادقة باستخدام دوال السياق
- تنفيذ التحقق من المدخلات مع Zod عبر إضافات Pothos
- ربط عميل React باستخدام urql لجلب البيانات
- التعامل مع الأخطاء وحالات التحميل بطريقة GraphQL
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 20+ مثبت (
node --version) - خبرة في TypeScript (الأنواع، الأنواع العامة، الواجهات)
- إلمام بـ Next.js App Router (معالجات المسارات، Server Components)
- أساسيات GraphQL — فهم الاستعلامات والتحولات والمخططات مفيد لكنه ليس مطلوبًا
- محرر أكواد — VS Code أو Cursor يُنصح بهما
لماذا GraphQL في 2026؟
يعمل REST جيدًا مع CRUD البسيط، لكن مع نمو الواجهة الأمامية، تواجه مشاكل مألوفة: جلب بيانات زائدة لا تحتاجها، جلب ناقص يتطلب طلبات إضافية، صعوبات في إدارة الإصدارات، وعدم وجود طريقة موحدة لوصف الواجهة البرمجية.
GraphQL يحل هذه المشاكل بالسماح للعميل بطلب ما يحتاجه بالضبط في طلب واحد. لكن إعدادات GraphQL التقليدية تتطلب صيانة ملفات SDL منفصلة وتشغيل مولدات أكواد للحفاظ على تزامن أنواع TypeScript.
Pothos يغير هذا. تعرّف المخطط في TypeScript، ويستنتج جميع الأنواع تلقائيًا. مع GraphQL Yoga (خادم خفيف ومتوافق مع المواصفات) و Next.js App Router، تحصل على واجهة GraphQL API آمنة الأنواع من قاعدة البيانات إلى واجهة المستخدم — بدون أي توليد أكواد.
ما ستبنيه
مدير إشارات مرجعية يتضمن:
- واجهة GraphQL API مكشوفة عبر معالجات مسارات Next.js
- استعلامات لعرض والبحث في الإشارات المرجعية
- تحولات لإنشاء وتحديث وحذف الإشارات المرجعية
- مصادقة المستخدم عبر السياق
- واجهة React أمامية تستخدم urql لاستهلاك الواجهة البرمجية
هيكل المشروع النهائي:
bookmark-app/
├── app/
│ ├── api/
│ │ └── graphql/
│ │ └── route.ts # معالج GraphQL Yoga
│ ├── bookmarks/
│ │ └── page.tsx # واجهة الإشارات المرجعية
│ └── layout.tsx
├── graphql/
│ ├── builder.ts # منشئ مخطط Pothos
│ ├── schema.ts # المخطط الرئيسي
│ ├── types/
│ │ ├── bookmark.ts # نوع الإشارة المرجعية + المحللات
│ │ └── user.ts # نوع المستخدم
│ └── context.ts # سياق الطلب
├── lib/
│ ├── db.ts # قاعدة بيانات في الذاكرة
│ └── urql.ts # إعداد عميل urql
└── package.json
الخطوة 1: إنشاء مشروع Next.js
أنشئ تطبيق Next.js 15 جديد مع TypeScript:
npx create-next-app@latest bookmark-app --typescript --tailwind --app --src-dir=false
cd bookmark-appاختر الإعدادات الافتراضية عند السؤال. ثم ثبّت تبعيات GraphQL:
npm install graphql graphql-yoga @pothos/core @pothos/plugin-zod zod
npm install urql @urql/next graphql-tagما يفعله كل حزمة:
| الحزمة | الغرض |
|---|---|
graphql | التنفيذ المرجعي لـ GraphQL |
graphql-yoga | خادم GraphQL خفيف ومتوافق مع المواصفات |
@pothos/core | منشئ مخططات code-first مع استدلال الأنواع |
@pothos/plugin-zod | تكامل التحقق من Zod لـ Pothos |
zod | التحقق من المخططات أثناء التشغيل |
urql | عميل GraphQL خفيف لـ React |
@urql/next | ربط urql الخاص بـ Next.js |
graphql-tag | قوالب نصية محددة لاستعلامات GraphQL |
الخطوة 2: إعداد قاعدة البيانات في الذاكرة
في هذا الدليل، ستستخدم مخزنًا في الذاكرة. في الإنتاج، استبدل هذا بـ Prisma أو Drizzle أو أي طبقة قاعدة بيانات.
أنشئ lib/db.ts:
export interface User {
id: string;
name: string;
email: string;
}
export interface Bookmark {
id: string;
url: string;
title: string;
description: string | null;
tags: string[];
userId: string;
createdAt: Date;
updatedAt: Date;
}
// بيانات أولية
const users: User[] = [
{ id: "1", name: "Alice", email: "alice@example.com" },
{ id: "2", name: "Bob", email: "bob@example.com" },
];
const bookmarks: Bookmark[] = [
{
id: "1",
url: "https://graphql.org",
title: "GraphQL Official Site",
description: "The official GraphQL documentation and specification",
tags: ["graphql", "api", "documentation"],
userId: "1",
createdAt: new Date("2026-01-15"),
updatedAt: new Date("2026-01-15"),
},
{
id: "2",
url: "https://nextjs.org",
title: "Next.js by Vercel",
description: "The React framework for the web",
tags: ["nextjs", "react", "framework"],
userId: "1",
createdAt: new Date("2026-02-01"),
updatedAt: new Date("2026-02-01"),
},
{
id: "3",
url: "https://pothos-graphql.dev",
title: "Pothos GraphQL",
description: "Code-first GraphQL schema builder for TypeScript",
tags: ["graphql", "typescript", "schema"],
userId: "2",
createdAt: new Date("2026-02-20"),
updatedAt: new Date("2026-02-20"),
},
];
let nextId = 4;
export const db = {
users: {
findMany: () => users,
findById: (id: string) => users.find((u) => u.id === id) ?? null,
findByEmail: (email: string) => users.find((u) => u.email === email) ?? null,
},
bookmarks: {
findMany: (filters?: { userId?: string; tag?: string; search?: string }) => {
let result = [...bookmarks];
if (filters?.userId) {
result = result.filter((b) => b.userId === filters.userId);
}
if (filters?.tag) {
result = result.filter((b) => b.tags.includes(filters.tag!));
}
if (filters?.search) {
const q = filters.search.toLowerCase();
result = result.filter(
(b) =>
b.title.toLowerCase().includes(q) ||
b.url.toLowerCase().includes(q) ||
b.description?.toLowerCase().includes(q)
);
}
return result;
},
findById: (id: string) => bookmarks.find((b) => b.id === id) ?? null,
create: (data: Omit<Bookmark, "id" | "createdAt" | "updatedAt">) => {
const bookmark: Bookmark = {
...data,
id: String(nextId++),
createdAt: new Date(),
updatedAt: new Date(),
};
bookmarks.push(bookmark);
return bookmark;
},
update: (id: string, data: Partial<Omit<Bookmark, "id" | "createdAt">>) => {
const index = bookmarks.findIndex((b) => b.id === id);
if (index === -1) return null;
bookmarks[index] = { ...bookmarks[index], ...data, updatedAt: new Date() };
return bookmarks[index];
},
delete: (id: string) => {
const index = bookmarks.findIndex((b) => b.id === id);
if (index === -1) return false;
bookmarks.splice(index, 1);
return true;
},
},
};الخطوة 3: تعريف سياق الطلب
سياق GraphQL هو المكان الذي تضع فيه البيانات الخاصة بكل طلب مثل المستخدم المصادق عليه واتصالات قاعدة البيانات.
أنشئ graphql/context.ts:
import { db, type User } from "@/lib/db";
export interface GraphQLContext {
currentUser: User | null;
db: typeof db;
}
export function createContext(request: Request): GraphQLContext {
// في الإنتاج، استخرج وتحقق من JWT أو رمز الجلسة هنا
const userId = request.headers.get("x-user-id");
const currentUser = userId ? db.users.findById(userId) : null;
return {
currentUser,
db,
};
}في هذا الدليل، نحاكي المصادقة بقراءة ترويسة x-user-id. في الإنتاج، ستتحقق من JWT أو تفحص ملف تعريف ارتباط الجلسة.
الخطوة 4: إنشاء منشئ مخطط Pothos
منشئ المخطط هو جوهر Pothos. هنا تُعدّ الإضافات وتعرّف نوع السياق وتُنشئ نسخة المنشئ.
أنشئ graphql/builder.ts:
import SchemaBuilder from "@pothos/core";
import ZodPlugin from "@pothos/plugin-zod";
import type { GraphQLContext } from "./context";
export const builder = new SchemaBuilder<{
Context: GraphQLContext;
Scalars: {
DateTime: {
Input: Date;
Output: Date;
};
};
}>({
plugins: [ZodPlugin],
});
// تسجيل نوع DateTime مخصص
builder.scalarType("DateTime", {
serialize: (value) => value.toISOString(),
parseValue: (value) => {
if (typeof value !== "string") {
throw new Error("DateTime must be a string");
}
return new Date(value);
},
});
// تهيئة أنواع Query و Mutation
builder.queryType({});
builder.mutationType({});لاحظ كيف تمرر GraphQLContext كنوع عام. هذا يعني أن كل محلل سيكون لديه وصول إلى currentUser و db مع استدلال أنواع كامل — بدون تحويل أنواع.
الخطوة 5: تعريف نوع المستخدم
أنشئ graphql/types/user.ts:
import { builder } from "../builder";
builder.objectType("User", {
description: "A registered user",
fields: (t) => ({
id: t.exposeString("id"),
name: t.exposeString("name"),
email: t.exposeString("email"),
bookmarks: t.field({
type: ["Bookmark"],
resolve: (user, _args, ctx) => {
return ctx.db.bookmarks.findMany({ userId: user.id });
},
}),
}),
});
// استعلام: الحصول على المستخدم الحالي
builder.queryField("me", (t) =>
t.field({
type: "User",
nullable: true,
resolve: (_root, _args, ctx) => ctx.currentUser,
})
);طريقة exposeString هي اختصار في Pothos يربط الحقل مباشرة بخاصية الكائن. للحقول المحسوبة مثل bookmarks، تكتب محللاً كاملاً.
الخطوة 6: تعريف نوع الإشارة المرجعية مع الاستعلامات
هذا هو النوع الرئيسي. أنشئ graphql/types/bookmark.ts:
import { builder } from "../builder";
import { z } from "zod";
// تعريف نوع كائن Bookmark
const BookmarkType = builder.objectType("Bookmark", {
description: "A saved bookmark with URL and metadata",
fields: (t) => ({
id: t.exposeString("id"),
url: t.exposeString("url"),
title: t.exposeString("title"),
description: t.exposeString("description", { nullable: true }),
tags: t.exposeStringList("tags"),
createdAt: t.expose("createdAt", { type: "DateTime" }),
updatedAt: t.expose("updatedAt", { type: "DateTime" }),
user: t.field({
type: "User",
nullable: true,
resolve: (bookmark, _args, ctx) => {
return ctx.db.users.findById(bookmark.userId);
},
}),
}),
});
// استعلام: عرض جميع الإشارات المرجعية مع فلاتر اختيارية
builder.queryField("bookmarks", (t) =>
t.field({
type: [BookmarkType],
args: {
tag: t.arg.string({ required: false }),
search: t.arg.string({ required: false }),
},
resolve: (_root, args, ctx) => {
return ctx.db.bookmarks.findMany({
tag: args.tag ?? undefined,
search: args.search ?? undefined,
});
},
})
);
// استعلام: الحصول على إشارة مرجعية واحدة بالمعرف
builder.queryField("bookmark", (t) =>
t.field({
type: BookmarkType,
nullable: true,
args: {
id: t.arg.string({ required: true }),
},
resolve: (_root, args, ctx) => {
return ctx.db.bookmarks.findById(args.id);
},
})
);الخطوة 7: إضافة التحولات مع التحقق بواسطة Zod
أضف تحولات الإنشاء والتحديث والحذف إلى نفس الملف graphql/types/bookmark.ts:
// مخططات Zod للتحقق من المدخلات
const CreateBookmarkInput = z.object({
url: z.string().url("Must be a valid URL"),
title: z.string().min(1, "Title is required").max(200),
description: z.string().max(1000).nullable().optional(),
tags: z.array(z.string().max(50)).max(10).default([]),
});
const UpdateBookmarkInput = z.object({
url: z.string().url("Must be a valid URL").optional(),
title: z.string().min(1).max(200).optional(),
description: z.string().max(1000).nullable().optional(),
tags: z.array(z.string().max(50)).max(10).optional(),
});
// تحول: إنشاء إشارة مرجعية
builder.mutationField("createBookmark", (t) =>
t.field({
type: BookmarkType,
args: {
input: t.arg({
type: builder.inputType("CreateBookmarkInput", {
fields: (t) => ({
url: t.string({ required: true }),
title: t.string({ required: true }),
description: t.string({ required: false }),
tags: t.stringList({ required: false, defaultValue: [] }),
}),
}),
required: true,
}),
},
resolve: (_root, args, ctx) => {
if (!ctx.currentUser) {
throw new Error("You must be logged in to create a bookmark");
}
const validated = CreateBookmarkInput.parse({
url: args.input.url,
title: args.input.title,
description: args.input.description,
tags: args.input.tags,
});
return ctx.db.bookmarks.create({
...validated,
description: validated.description ?? null,
tags: validated.tags,
userId: ctx.currentUser.id,
});
},
})
);
// تحول: تحديث إشارة مرجعية
builder.mutationField("updateBookmark", (t) =>
t.field({
type: BookmarkType,
nullable: true,
args: {
id: t.arg.string({ required: true }),
input: t.arg({
type: builder.inputType("UpdateBookmarkInput", {
fields: (t) => ({
url: t.string({ required: false }),
title: t.string({ required: false }),
description: t.string({ required: false }),
tags: t.stringList({ required: false }),
}),
}),
required: true,
}),
},
resolve: (_root, args, ctx) => {
if (!ctx.currentUser) {
throw new Error("You must be logged in to update a bookmark");
}
const existing = ctx.db.bookmarks.findById(args.id);
if (!existing || existing.userId !== ctx.currentUser.id) {
throw new Error("Bookmark not found or not authorized");
}
const validated = UpdateBookmarkInput.parse({
url: args.input.url,
title: args.input.title,
description: args.input.description,
tags: args.input.tags,
});
return ctx.db.bookmarks.update(args.id, validated);
},
})
);
// تحول: حذف إشارة مرجعية
builder.mutationField("deleteBookmark", (t) =>
t.field({
type: "Boolean",
args: {
id: t.arg.string({ required: true }),
},
resolve: (_root, args, ctx) => {
if (!ctx.currentUser) {
throw new Error("You must be logged in to delete a bookmark");
}
const existing = ctx.db.bookmarks.findById(args.id);
if (!existing || existing.userId !== ctx.currentUser.id) {
throw new Error("Bookmark not found or not authorized");
}
return ctx.db.bookmarks.delete(args.id);
},
})
);كل تحول يتحقق من ctx.currentUser قبل المتابعة. هذا هو المكافئ لوسيط middleware في GraphQL — تتحكم في الوصول على مستوى المحلل.
مخططات Zod تتحقق من المدخلات أثناء التشغيل. إذا أرسل العميل عنوان URL غير صالح أو عنوانًا طويلاً جدًا، يحصل على رسالة خطأ واضحة.
الخطوة 8: تجميع المخطط
أنشئ graphql/schema.ts لجمع كل شيء معًا:
import { builder } from "./builder";
// استيراد الأنواع لتسجيلها مع المنشئ
import "./types/user";
import "./types/bookmark";
// بناء وتصدير المخطط القابل للتنفيذ
export const schema = builder.toSchema();الاستيرادات لها تأثيرات جانبية — تسجل الأنواع مع المنشئ عند تحميل الوحدة. هذا نمط شائع في Pothos.
الخطوة 9: إنشاء معالج مسار GraphQL
الآن اكشف المخطط كمسار API في Next.js. أنشئ app/api/graphql/route.ts:
import { createYoga } from "graphql-yoga";
import { schema } from "@/graphql/schema";
import { createContext } from "@/graphql/context";
const yoga = createYoga({
schema,
context: ({ request }) => createContext(request),
graphqlEndpoint: "/api/graphql",
graphiql: process.env.NODE_ENV === "development",
fetchAPI: { Response },
});
const handler = yoga;
export { handler as GET, handler as POST };هذا كل ما تحتاجه. GraphQL Yoga يتعامل مع تحليل الاستعلامات والتحقق منها والتنفيذ ومعالجة الأخطاء وواجهة GraphiQL للتطوير.
الخطوة 10: الاختبار مع GraphiQL
شغّل خادم التطوير:
npm run devافتح http://localhost:3000/api/graphql في المتصفح. سترى واجهة GraphiQL. جرّب هذه الاستعلامات:
عرض جميع الإشارات المرجعية:
query {
bookmarks {
id
title
url
tags
user {
name
}
}
}البحث في الإشارات المرجعية:
query {
bookmarks(search: "react") {
title
url
description
}
}التصفية حسب الوسم:
query {
bookmarks(tag: "graphql") {
title
tags
}
}للتحولات، أضف ترويسة x-user-id في لوحة الترويسات في GraphiQL:
{
"x-user-id": "1"
}ثم نفّذ:
mutation {
createBookmark(input: {
url: "https://yoga-graphql.com"
title: "GraphQL Yoga"
description: "A fully-featured GraphQL server"
tags: ["graphql", "server"]
}) {
id
title
url
tags
createdAt
}
}الخطوة 11: إعداد عميل urql
الآن ابنِ واجهة React أمامية لاستهلاك الواجهة البرمجية. أنشئ lib/urql.ts:
import { cacheExchange, createClient, fetchExchange } from "@urql/next";
export function makeClient() {
return createClient({
url: "/api/graphql",
exchanges: [cacheExchange, fetchExchange],
fetchOptions: () => ({
headers: {
"x-user-id": "1",
},
}),
});
}أنشئ مكون المزود في app/providers.tsx:
"use client";
import { UrqlProvider, ssrExchange } from "@urql/next";
import { useMemo } from "react";
import { makeClient } from "@/lib/urql";
export function Providers({ children }: { children: React.ReactNode }) {
const [client, ssr] = useMemo(() => {
const ssr = ssrExchange({ isClient: typeof window !== "undefined" });
const client = makeClient();
return [client, ssr];
}, []);
return (
<UrqlProvider client={client} ssr={ssr}>
{children}
</UrqlProvider>
);
}الخطوة 12: بناء صفحة الإشارات المرجعية
أنشئ app/bookmarks/page.tsx:
"use client";
import { useQuery, useMutation } from "@urql/next";
import { gql } from "graphql-tag";
import { useState } from "react";
const BOOKMARKS_QUERY = gql`
query Bookmarks($search: String, $tag: String) {
bookmarks(search: $search, tag: $tag) {
id
title
url
description
tags
createdAt
user {
name
}
}
}
`;
const CREATE_BOOKMARK = gql`
mutation CreateBookmark($input: CreateBookmarkInput!) {
createBookmark(input: $input) {
id
title
url
tags
}
}
`;
const DELETE_BOOKMARK = gql`
mutation DeleteBookmark($id: String!) {
deleteBookmark(id: $id)
}
`;
export default function BookmarksPage() {
const [search, setSearch] = useState("");
const [selectedTag, setSelectedTag] = useState("");
const [result, reexecute] = useQuery({
query: BOOKMARKS_QUERY,
variables: {
search: search || null,
tag: selectedTag || null,
},
});
const [, createBookmark] = useMutation(CREATE_BOOKMARK);
const [, deleteBookmark] = useMutation(DELETE_BOOKMARK);
const { data, fetching, error } = result;
async function handleCreate(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = new FormData(e.currentTarget);
await createBookmark({
input: {
url: form.get("url") as string,
title: form.get("title") as string,
description: (form.get("description") as string) || null,
tags: (form.get("tags") as string)
.split(",")
.map((t) => t.trim())
.filter(Boolean),
},
});
e.currentTarget.reset();
reexecute({ requestPolicy: "network-only" });
}
async function handleDelete(id: string) {
await deleteBookmark({ id });
reexecute({ requestPolicy: "network-only" });
}
const allTags = [
...new Set(data?.bookmarks?.flatMap((b: any) => b.tags) ?? []),
];
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">الإشارات المرجعية</h1>
<div className="flex gap-4 mb-6">
<input
type="text"
placeholder="البحث في الإشارات المرجعية..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="flex-1 px-4 py-2 border rounded-lg"
/>
<select
value={selectedTag}
onChange={(e) => setSelectedTag(e.target.value)}
className="px-4 py-2 border rounded-lg"
>
<option value="">جميع الوسوم</option>
{allTags.map((tag) => (
<option key={tag} value={tag}>
{tag}
</option>
))}
</select>
</div>
<form onSubmit={handleCreate} className="mb-8 p-4 bg-gray-50 rounded-lg">
<h2 className="text-lg font-semibold mb-4">إضافة إشارة مرجعية</h2>
<div className="grid grid-cols-2 gap-4">
<input name="url" placeholder="الرابط" required className="px-3 py-2 border rounded" />
<input name="title" placeholder="العنوان" required className="px-3 py-2 border rounded" />
<input name="description" placeholder="الوصف (اختياري)" className="px-3 py-2 border rounded" />
<input name="tags" placeholder="الوسوم (مفصولة بفواصل)" className="px-3 py-2 border rounded" />
</div>
<button type="submit" className="mt-4 px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
إضافة
</button>
</form>
{fetching && <p>جاري التحميل...</p>}
{error && <p className="text-red-500">خطأ: {error.message}</p>}
{data?.bookmarks && (
<div className="space-y-4">
{data.bookmarks.map((bookmark: any) => (
<div key={bookmark.id} className="p-4 border rounded-lg hover:shadow-md transition-shadow">
<div className="flex justify-between items-start">
<div>
<a
href={bookmark.url}
target="_blank"
rel="noopener noreferrer"
className="text-lg font-semibold text-blue-600 hover:underline"
>
{bookmark.title}
</a>
<p className="text-sm text-gray-500 mt-1">{bookmark.url}</p>
{bookmark.description && (
<p className="text-gray-700 mt-2">{bookmark.description}</p>
)}
<div className="flex gap-2 mt-2">
{bookmark.tags.map((tag: string) => (
<span
key={tag}
className="px-2 py-1 bg-gray-200 rounded-full text-xs cursor-pointer hover:bg-gray-300"
onClick={() => setSelectedTag(tag)}
>
{tag}
</span>
))}
</div>
<p className="text-xs text-gray-400 mt-2">
بواسطة {bookmark.user?.name} · {new Date(bookmark.createdAt).toLocaleDateString("ar")}
</p>
</div>
<button
onClick={() => handleDelete(bookmark.id)}
className="text-red-500 hover:text-red-700 text-sm"
>
حذف
</button>
</div>
</div>
))}
</div>
)}
</div>
);
}الخطوة 13: إضافة معالجة الأخطاء
GraphQL لديه نظام أخطاء مدمج. أنشئ graphql/errors.ts:
import { GraphQLError } from "graphql";
export class AuthenticationError extends GraphQLError {
constructor(message = "You must be logged in") {
super(message, {
extensions: { code: "UNAUTHENTICATED" },
});
}
}
export class AuthorizationError extends GraphQLError {
constructor(message = "You are not authorized to perform this action") {
super(message, {
extensions: { code: "FORBIDDEN" },
});
}
}
export class NotFoundError extends GraphQLError {
constructor(resource: string) {
super(`${resource} not found`, {
extensions: { code: "NOT_FOUND" },
});
}
}حقل extensions.code يسمح للعميل بالتعامل مع الأخطاء برمجيًا:
if (error.graphQLErrors.some((e) => e.extensions?.code === "UNAUTHENTICATED")) {
// إعادة التوجيه لتسجيل الدخول
}الخطوة 14: إضافة التصفح المرقّم
لواجهات API الإنتاجية، تحتاج التصفح المرقّم. أضف تصفحًا مرقّمًا قائمًا على المؤشر:
query {
paginatedBookmarks(first: 2) {
edges {
cursor
node {
title
url
}
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}للصفحة التالية، مرر after: "last-cursor-value".
استكشاف الأخطاء وإصلاحها
"Cannot find module @/graphql/schema"
تأكد من أن tsconfig.json يحتوي على إعداد مسار @/*:
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}GraphiQL لا يتحمّل
GraphiQL مفعّل فقط في وضع التطوير. تحقق من أن NODE_ENV ليس معينًا على production.
أخطاء "You must be logged in"
للتحولات، أضف ترويسة x-user-id: 1 في GraphiQL أو في إعداد عميل urql.
الخطوات التالية
لديك الآن واجهة GraphQL API آمنة الأنواع بالكامل مع Next.js. إليك طرقًا لتوسيعها:
- إضافة قاعدة بيانات حقيقية — استبدل المخزن في الذاكرة بـ Prisma أو Drizzle ORM متصل بـ PostgreSQL
- تنفيذ المصادقة — استخدم NextAuth.js أو Better Auth لإصدار رموز حقيقية
- إضافة اشتراكات — GraphQL Yoga يدعم اشتراكات WebSocket للتحديثات الفورية
- توليد أنواع العميل — استخدم
graphql-codegenلتوليد hooks مكتوبة للواجهة الأمامية - النشر — GraphQL Yoga يعمل على Vercel و Cloudflare Workers وأي منصة Node.js
الخلاصة
في هذا الدليل، بنيت واجهة GraphQL API كاملة باستخدام Next.js App Router و GraphQL Yoga و Pothos. تعلمت كيفية:
- تعريف مخطط code-first مع استدلال TypeScript كامل باستخدام Pothos
- إعداد GraphQL Yoga كمعالج مسار Next.js
- بناء استعلامات مع تصفية و تحولات مع التحقق بواسطة Zod
- تنفيذ المصادقة عبر السياق ومعالجة أخطاء منظمة
- إضافة تصفح مرقّم قائم على المؤشر للاستخدام الإنتاجي
- ربط واجهة React أمامية مع urql لجلب بيانات آمن الأنواع
الجمع بين Pothos و GraphQL Yoga يمنحك أفضل ما في العالمين: مرونة GraphQL مع أمان أنواع TypeScript — بدون توليد أكواد، بدون ملفات SDL، فقط TypeScript من البداية للنهاية.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء واجهات برمجة تطبيقات آمنة الأنواع من البداية للنهاية مع tRPC و Next.js App Router
تعلم كيفية بناء واجهات برمجة تطبيقات آمنة الأنواع بالكامل مع tRPC و Next.js 15 App Router. يغطي هذا الدليل العملي إعداد الموجه والإجراءات والوسيط وتكامل React Query والاستدعاءات من جانب الخادم — كل ذلك دون كتابة أي مخطط API.

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

بناء وكيل ذكاء اصطناعي مستقل باستخدام Agentic RAG و Next.js
تعلم كيف تبني وكيل ذكاء اصطناعي يقرر بشكل مستقل متى وكيف يسترجع المعلومات من قواعد البيانات المتجهية. دليل عملي شامل باستخدام Vercel AI SDK و Next.js مع أمثلة قابلة للتنفيذ.