Nitro + H3: بناء واجهات برمجة تطبيقات TypeScript شاملة تعمل في كل مكان

AI Bot
بواسطة AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

قاعدة كود واحدة. كل بيئة تشغيل. Nitro هو محرك الخادم مفتوح المصدر المبني بـ TypeScript الذي يعمل خلف Nuxt و Analog و Vinxi. يقوم بتجميع مسارات API الخاصة بك إلى حزم محسّنة لـ Node.js و Cloudflare Workers و Vercel Edge و Deno و Bun والمزيد — دون تغيير أي إعداد. في هذا الدرس، ستبني واجهة برمجة تطبيقات REST كاملة وتنشرها في كل مكان.

ما ستتعلمه

بنهاية هذا الدرس، ستكون قادراً على:

  • إعداد مشروع Nitro مستقل من الصفر مع TypeScript
  • بناء مسارات API مع طلبات واستجابات محددة النوع باستخدام H3
  • إنشاء برمجيات وسيطة للمصادقة والتسجيل و CORS
  • تكامل قاعدة بيانات باستخدام طبقة التخزين المدمجة في Nitro
  • تنفيذ نقاط WebSocket للميزات الفورية
  • إضافة مهام مجدولة (cron jobs) تعمل على جانب الخادم
  • استخدام إضافات الخادم وخطافات دورة الحياة
  • نشر نفس قاعدة الكود على Node.js و Cloudflare Workers و Vercel و Deno

المتطلبات الأساسية

قبل البدء، تأكد من توفر:

  • Node.js 20+ مثبت (node --version)
  • خبرة في TypeScript (الأنواع، async/await)
  • مفاهيم REST API الأساسية (طرق HTTP، رموز الحالة)
  • محرر أكواد — VS Code أو Cursor موصى بهما
  • حساب Cloudflare (اختياري، للنشر على Workers)

لماذا Nitro؟

معظم أطر عمل خوادم TypeScript تقيّدك ببيئة تشغيل واحدة. Express يعمل على Node.js فقط. Hono يستهدف بيئات الحافة. Nitro مختلف — يقوم بتجميع كود الخادم الخاص بك إلى حزم محسّنة لـ أي بيئة تشغيل JavaScript:

الميزةNitroExpressHonoFastify
متعدد البيئاتأكثر من 15 إعداد مسبقNode.js فقطجزئيNode.js فقط
توجيه قائم على الملفاتمدمجيدوييدوييدوي
استيراد تلقائينعملالالا
تخزين مدمجKV, FS, Redis, D1يدوييدوييدوي
WebSocketsمدمجحزمة wsمحولإضافة
مهام مجدولةمدمجnode-cronغير مدمجغير مدمج
إزالة الكود غير المستخدمتلقائيلالالا
إعادة تحميل فوريخادم تطوير مدمجnodemonيدوييدوي

Nitro مدعوم بـ H3، إطار عمل HTTP مصغّر مبني للأداء العالي. H3 يتعامل مع تحليل الطلبات والتوجيه والاستجابة — بينما يضيف Nitro التوجيه القائم على الملفات والاستيراد التلقائي وتحسين البناء وإعدادات النشر المسبقة.


الخطوة 1: إنشاء مشروع Nitro جديد

أنشئ مشروع Nitro جديد:

npx giget@latest nitro nitro-api && cd nitro-api
npm install

هيكل المشروع يبدو هكذا:

nitro-api/
├── routes/
│   └── index.ts        # GET /
├── nitro.config.ts      # إعدادات Nitro
├── tsconfig.json
└── package.json

شغّل خادم التطوير:

npx nitropack dev

زر http://localhost:3000 — سترى الاستجابة الافتراضية. خادم التطوير يحتوي على إعادة تحميل الوحدات الفوري مدمج، لذا التغييرات تظهر فوراً.


الخطوة 2: بناء مسارات API مع H3

Nitro يستخدم التوجيه القائم على الملفات. كل ملف في مجلد routes/ يصبح نقطة نهاية API. مسار الملف يُطابق مباشرة مسار URL.

المسارات الأساسية

أنشئ routes/api/health.ts:

export default defineEventHandler(() => {
  return { status: "ok", timestamp: Date.now() };
});

يستجيب لـ GET /api/health. Nitro يستورد تلقائياً defineEventHandler — لا حاجة لجملة استيراد.

معاملات المسار

أنشئ routes/api/users/[id].ts:

export default defineEventHandler((event) => {
  const id = getRouterParam(event, "id");
 
  return {
    user: {
      id,
      name: `User ${id}`,
      email: `user${id}@example.com`,
    },
  };
});

الوصول إليه عبر GET /api/users/42. [id] في اسم الملف يصبح معامل ديناميكي.

التوجيه حسب طريقة HTTP

أنشئ معالجات خاصة بكل طريقة عبر تسمية الملفات بلاحقة الطريقة:

routes/
└── api/
    └── posts/
        ├── index.get.ts     # GET /api/posts
        ├── index.post.ts    # POST /api/posts
        └── [id].put.ts      # PUT /api/posts/:id
        └── [id].delete.ts   # DELETE /api/posts/:id

أنشئ routes/api/posts/index.get.ts:

export default defineEventHandler(() => {
  return {
    posts: [
      { id: 1, title: "البدء مع Nitro", published: true },
      { id: 2, title: "H3 بالتفصيل", published: false },
    ],
  };
});

أنشئ routes/api/posts/index.post.ts:

export default defineEventHandler(async (event) => {
  const body = await readBody(event);
 
  if (!body.title) {
    throw createError({
      statusCode: 400,
      statusMessage: "العنوان مطلوب",
    });
  }
 
  return {
    post: {
      id: Math.floor(Math.random() * 1000),
      title: body.title,
      content: body.content || "",
      createdAt: new Date().toISOString(),
    },
  };
});

التحقق من الطلبات مع الأنواع

للتحقق القوي من المدخلات، استخدم readValidatedBody من H3 مع مكتبة تحقق:

npm install zod

أنشئ routes/api/posts/index.post.ts مع تحقق Zod:

import { z } from "zod";
 
const PostSchema = z.object({
  title: z.string().min(3).max(200),
  content: z.string().optional().default(""),
  tags: z.array(z.string()).optional().default([]),
  published: z.boolean().optional().default(false),
});
 
export default defineEventHandler(async (event) => {
  const body = await readValidatedBody(event, PostSchema.parse);
 
  return {
    post: {
      id: Math.floor(Math.random() * 1000),
      ...body,
      createdAt: new Date().toISOString(),
    },
  };
});

إذا فشل التحقق، H3 يُرجع تلقائياً خطأ 400 مع التفاصيل.


الخطوة 3: البرمجيات الوسيطة والأدوات المساعدة

برمجية وسيطة للطلبات

أنشئ ملفات في مجلد middleware/ تعمل قبل كل معالج مسار.

أنشئ middleware/logger.ts:

export default defineEventHandler((event) => {
  const method = getMethod(event);
  const path = getRequestURL(event).pathname;
  console.log(`[${new Date().toISOString()}] ${method} ${path}`);
});

يسجل كل طلب وارد. معالجات البرمجيات الوسيطة التي لا تُرجع قيمة تمرر التحكم للمعالج التالي.

برمجية وسيطة خاصة بالمسار

أنشئ middleware/api/auth.ts — تعمل فقط لمسارات /api/*:

export default defineEventHandler((event) => {
  const authHeader = getHeader(event, "authorization");
 
  if (!authHeader?.startsWith("Bearer ")) {
    throw createError({
      statusCode: 401,
      statusMessage: "رأس التفويض مفقود أو غير صالح",
    });
  }
 
  const token = authHeader.slice(7);
 
  // في الإنتاج، تحقق من JWT هنا
  event.context.userId = token;
});

الوصول لمعرف المستخدم في أي مسار API عبر event.context.userId.

إعداد CORS

Nitro يدعم CORS مدمجاً. حدّث nitro.config.ts:

export default defineNitroConfig({
  routeRules: {
    "/api/**": {
      cors: true,
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
        "Access-Control-Allow-Headers": "Content-Type, Authorization",
      },
    },
  },
});

الأدوات المساعدة المشتركة

أنشئ أدوات قابلة لإعادة الاستخدام في مجلد utils/. يتم استيرادها تلقائياً في كل المشروع.

أنشئ utils/response.ts:

export function successResponse<T>(data: T, message = "نجاح") {
  return {
    success: true,
    message,
    data,
  };
}
 
export function paginatedResponse<T>(
  data: T[],
  page: number,
  limit: number,
  total: number
) {
  return {
    success: true,
    data,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit),
    },
  };
}

استخدمها في أي مسار دون استيراد:

export default defineEventHandler(() => {
  return successResponse({ version: "1.0.0" });
});

الخطوة 4: تكامل قاعدة البيانات مع طبقة التخزين

Nitro يتضمن طبقة تخزين شاملة مدعومة بـ unstorage. توفر واجهة مفتاح-قيمة تعمل مع عدة خلفيات — نظام الملفات، Redis، Cloudflare KV، Vercel KV، والمزيد.

إعداد التخزين

حدّث nitro.config.ts:

export default defineNitroConfig({
  storage: {
    posts: {
      driver: "fs",
      base: ".data/posts",
    },
    cache: {
      driver: "memory",
    },
  },
});

استخدام التخزين في المسارات

أنشئ routes/api/posts/index.get.ts:

export default defineEventHandler(async () => {
  const keys = await useStorage("posts").getKeys();
 
  const posts = await Promise.all(
    keys.map(async (key) => {
      return await useStorage("posts").getItem(key);
    })
  );
 
  return successResponse(posts.filter(Boolean));
});

أنشئ routes/api/posts/index.post.ts:

import { z } from "zod";
 
const PostSchema = z.object({
  title: z.string().min(3),
  content: z.string(),
  tags: z.array(z.string()).default([]),
});
 
export default defineEventHandler(async (event) => {
  const body = await readValidatedBody(event, PostSchema.parse);
  const id = `post-${Date.now()}`;
 
  const post = {
    id,
    ...body,
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
  };
 
  await useStorage("posts").setItem(id, post);
 
  setResponseStatus(event, 201);
  return successResponse(post, "تم إنشاء المقال");
});

أنشئ routes/api/posts/[id].get.ts:

export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, "id");
  const post = await useStorage("posts").getItem(id!);
 
  if (!post) {
    throw createError({
      statusCode: 404,
      statusMessage: "المقال غير موجود",
    });
  }
 
  return successResponse(post);
});

أنشئ routes/api/posts/[id].delete.ts:

export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, "id");
  const exists = await useStorage("posts").hasItem(id!);
 
  if (!exists) {
    throw createError({
      statusCode: 404,
      statusMessage: "المقال غير موجود",
    });
  }
 
  await useStorage("posts").removeItem(id!);
 
  return successResponse(null, "تم حذف المقال");
});

التبديل إلى Redis في الإنتاج

لاستخدام Redis بدلاً من نظام الملفات، غيّر إعداد التخزين:

export default defineNitroConfig({
  storage: {
    posts: {
      driver: "redis",
      url: process.env.REDIS_URL || "redis://localhost:6379",
    },
  },
});

لا حاجة لتغيير أي كود في المسارات — واجهة التخزين نفسها بغض النظر عن الخلفية.


الخطوة 5: التخزين المؤقت مع قواعد المسارات

Nitro يحتوي على تخزين مؤقت مدمج. خزّن استجابات API مؤقتاً دون تغيير الكود باستخدام قواعد المسارات:

export default defineNitroConfig({
  routeRules: {
    "/api/posts": {
      cache: {
        maxAge: 60, // 60 ثانية
      },
    },
    "/api/health": {
      cache: {
        maxAge: 10,
      },
    },
    "/api/posts/**": {
      cache: false, // بدون تخزين مؤقت للمقالات الفردية
    },
  },
});

للتخزين المؤقت البرمجي، استخدم cachedEventHandler:

export default cachedEventHandler(
  async () => {
    // هذه العملية المكلفة مخزنة مؤقتاً
    const allPosts = await fetchAllPosts();
    const stats = computeStats(allPosts);
    return successResponse(stats);
  },
  {
    maxAge: 300, // 5 دقائق
    name: "post-stats",
  }
);

الخطوة 6: دعم WebSocket

Nitro يدعم WebSockets من خلال أداة defineWebSocketHandler.

أنشئ routes/ws.ts:

export default defineWebSocketHandler({
  open(peer) {
    console.log(`[ws] عميل متصل: ${peer.id}`);
    peer.send(JSON.stringify({ type: "welcome", id: peer.id }));
    peer.subscribe("chat");
  },
 
  message(peer, message) {
    const data = JSON.parse(message.text());
    console.log(`[ws] رسالة من ${peer.id}:`, data);
 
    // بث لجميع المشتركين
    peer.publish(
      "chat",
      JSON.stringify({
        type: "message",
        from: peer.id,
        text: data.text,
        timestamp: Date.now(),
      })
    );
  },
 
  close(peer) {
    console.log(`[ws] عميل انقطع: ${peer.id}`);
  },
});

فعّل دعم WebSocket في nitro.config.ts:

export default defineNitroConfig({
  experimental: {
    websocket: true,
  },
});

الاتصال من عميل:

const ws = new WebSocket("ws://localhost:3000/ws");
 
ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log("تم الاستلام:", data);
};
 
ws.onopen = () => {
  ws.send(JSON.stringify({ text: "مرحباً من العميل!" }));
};

الخطوة 7: المهام المجدولة

Nitro يدعم المهام المجدولة المشابهة لـ cron التي تعمل على جانب الخادم.

أنشئ tasks/cleanup.ts:

export default defineTask({
  meta: {
    name: "cleanup",
    description: "إزالة البيانات المنتهية من التخزين",
  },
  async run() {
    const keys = await useStorage("posts").getKeys();
    let removed = 0;
 
    for (const key of keys) {
      const post: any = await useStorage("posts").getItem(key);
      if (!post) continue;
 
      const age = Date.now() - new Date(post.createdAt).getTime();
      const thirtyDays = 30 * 24 * 60 * 60 * 1000;
 
      if (age > thirtyDays && !post.published) {
        await useStorage("posts").removeItem(key);
        removed++;
      }
    }
 
    return { result: `تمت إزالة ${removed} مسودة منتهية` };
  },
});

إعداد الجدولة في nitro.config.ts:

export default defineNitroConfig({
  experimental: {
    tasks: true,
  },
  scheduledTasks: {
    // تشغيل التنظيف كل يوم عند منتصف الليل
    "0 0 * * *": ["cleanup"],
  },
});

أثناء التطوير، شغّل المهام يدوياً عبر نقطة النهاية المدمجة:

curl http://localhost:3000/_nitro/tasks/cleanup

الخطوة 8: إضافات الخادم ودورة الحياة

أنشئ إضافات تعمل عند بدء تشغيل الخادم. استخدمها لاتصالات قواعد البيانات أو تهيئة الخدمات أو التسجيل.

أنشئ plugins/startup.ts:

export default defineNitroPlugin((nitroApp) => {
  console.log("[plugin] الخادم يبدأ التشغيل...");
 
  // ربط بدورة حياة الطلب
  nitroApp.hooks.hook("request", (event) => {
    event.context.requestStartTime = Date.now();
  });
 
  nitroApp.hooks.hook("afterResponse", (event) => {
    const duration = Date.now() - (event.context.requestStartTime || 0);
    const path = getRequestURL(event).pathname;
    console.log(`[perf] ${path} - ${duration}ms`);
  });
 
  // ربط بإغلاق الخادم
  nitroApp.hooks.hook("close", () => {
    console.log("[plugin] الخادم يتوقف...");
  });
});

الخطوة 9: معالجة الأخطاء

أنشئ معالج أخطاء عام لتوحيد استجابات الأخطاء.

أنشئ plugins/error-handler.ts:

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook("error", (error, { event }) => {
    console.error(`[error] ${error.message}`, {
      path: event ? getRequestURL(event).pathname : "unknown",
      stack: error.stack,
    });
  });
});

أنشئ استجابات خطأ مخصصة في المسارات:

export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, "id");
 
  if (!id || !/^\d+$/.test(id)) {
    throw createError({
      statusCode: 400,
      statusMessage: "تنسيق المعرف غير صالح",
      data: {
        field: "id",
        expected: "سلسلة رقمية",
        received: id,
      },
    });
  }
 
  // متابعة المعالجة...
});

الخطوة 10: النشر في كل مكان

هنا يتألق Nitro حقاً. نفس قاعدة الكود تُنشر على أي منصة بتغيير قيمة إعداد واحدة.

النشر على Node.js (الافتراضي)

npx nitropack build
node .output/server/index.mjs

مخرجات البناء عبارة عن خادم مستقل في .output/ — لا حاجة لـ node_modules في الإنتاج.

النشر على Cloudflare Workers

حدّث nitro.config.ts:

export default defineNitroConfig({
  preset: "cloudflare-pages",
});

بناء ونشر:

npx nitropack build
npx wrangler pages deploy .output/public

النشر على Vercel

export default defineNitroConfig({
  preset: "vercel-edge",
});

ادفع إلى Git — Vercel يكتشف تلقائياً ويُنشر.

النشر على Deno Deploy

export default defineNitroConfig({
  preset: "deno-deploy",
});
npx nitropack build
deployctl deploy --project=my-api .output/server/index.ts

النشر على Bun

export default defineNitroConfig({
  preset: "bun",
});
npx nitropack build
bun .output/server/index.mjs

جميع الإعدادات المسبقة المتاحة

Nitro يدعم أكثر من 15 هدف نشر:

الإعداد المسبقالمنصة
nodeخادم Node.js مستقل
bunبيئة تشغيل Bun
deno-deployDeno Deploy
cloudflare-pagesCloudflare Pages
cloudflare-moduleCloudflare Workers
vercelVercel Serverless Functions
vercel-edgeVercel Edge Functions
netlifyNetlify Functions
netlify-edgeNetlify Edge Functions
aws-lambdaAWS Lambda
firebaseFirebase Functions
digital-oceanDigitalOcean App Platform
renderRender.com
dockerحاوية Docker

الخطوة 11: إعداد الإنتاج

أنشئ إعداداً جاهزاً للإنتاج:

export default defineNitroConfig({
  // هدف النشر
  preset: process.env.NITRO_PRESET || "node",
 
  // تخزين مؤقت على مستوى المسار
  routeRules: {
    "/api/**": {
      cors: true,
      headers: {
        "Access-Control-Allow-Origin": "*",
      },
    },
    "/api/posts": {
      cache: { maxAge: 60 },
    },
  },
 
  // خلفيات التخزين
  storage: {
    posts: {
      driver: process.env.REDIS_URL ? "redis" : "fs",
      ...(process.env.REDIS_URL
        ? { url: process.env.REDIS_URL }
        : { base: ".data/posts" }),
    },
  },
 
  // إعدادات وقت التشغيل (الوصول إليها عبر useRuntimeConfig())
  runtimeConfig: {
    apiSecret: process.env.API_SECRET || "dev-secret",
    public: {
      apiBase: process.env.API_BASE || "http://localhost:3000",
    },
  },
 
  // تفعيل الميزات التجريبية
  experimental: {
    websocket: true,
    tasks: true,
  },
 
  // المهام المجدولة
  scheduledTasks: {
    "0 0 * * *": ["cleanup"],
  },
 
  // الضغط
  compressPublicAssets: true,
});

الوصول لإعدادات وقت التشغيل في المسارات:

export default defineEventHandler(() => {
  const config = useRuntimeConfig();
  return {
    apiBase: config.public.apiBase,
    // لا تكشف أبداً الأسرار في الاستجابات
  };
});

اختبار واجهة API

اختبر واجهة API الكاملة باستخدام curl:

# فحص الصحة
curl http://localhost:3000/api/health
 
# إنشاء مقال
curl -X POST http://localhost:3000/api/posts \
  -H "Content-Type: application/json" \
  -d '{"title": "مقالي الأول", "content": "مرحباً Nitro!", "tags": ["مقدمة"]}'
 
# عرض جميع المقالات
curl http://localhost:3000/api/posts
 
# الحصول على مقال محدد (استخدم المعرف من استجابة الإنشاء)
curl http://localhost:3000/api/posts/post-1712140800000
 
# حذف مقال
curl -X DELETE http://localhost:3000/api/posts/post-1712140800000

استكشاف الأخطاء وإصلاحها

أخطاء "Cannot find module"

Nitro يستورد تلقائياً الأدوات من H3 ومن بيئة تشغيله. إذا أظهر محررك أخطاء، تأكد أن tsconfig.json يمتد من الأنواع المولّدة:

{
  "extends": "./.nitro/types/tsconfig.json"
}

شغّل npx nitropack prepare لتوليد الأنواع.

التخزين لا يحفظ البيانات

في التطوير، مشغّل نظام الملفات يخزن البيانات في .data/. تأكد أن هذا المجلد موجود وقابل للكتابة. للإنتاج، استخدم Redis أو مشغّل تخزين سحابي.

رفض اتصال WebSocket

تأكد أن experimental.websocket مضبوط على true في nitro.config.ts. بعض منصات النشر (مثل Cloudflare Pages) تتطلب إعداداً خاصاً لدعم WebSocket.

مخرجات البناء كبيرة جداً

Nitro يزيل الكود غير المستخدم تلقائياً. إذا كانت المخرجات لا تزال كبيرة، تحقق من التبعيات الثقيلة المستوردة على المستوى الأعلى. استخدم الاستيراد الديناميكي للميزات الاختيارية:

export default defineEventHandler(async () => {
  const { heavyLibrary } = await import("heavy-library");
  return heavyLibrary.process();
});

الخطوات التالية

  • أضف قاعدة بيانات: اربط Drizzle ORM أو Prisma لدعم قواعد بيانات SQL
  • أضف مصادقة: نفّذ التحقق من JWT في البرمجيات الوسيطة
  • أضف تحديد المعدل: استخدم طبقة التخزين المؤقت في Nitro مع unstorage لتحديد المعدل
  • ابنِ واجهة أمامية: اقرنه مع أي إطار عمل واجهة أمامية — React، Vue، Svelte، أو HTML عادي
  • استكشف Nuxt: إذا كنت بحاجة لإطار عمل متكامل، Nuxt يستخدم Nitro كمحرك خادمه بنفس الواجهات

الخلاصة

Nitro و H3 يمنحانك ميزة فريدة في منظومة خوادم TypeScript: اكتب مرة واحدة، انشر في أي مكان. بنيت واجهة برمجة تطبيقات REST كاملة مع تحقق محدد النوع، برمجيات وسيطة، تخزين مؤقت، WebSockets، مهام مجدولة، وطبقة تخزين شاملة. نفس قاعدة الكود تعمل على Node.js و Cloudflare Workers و Vercel Edge و Deno و Bun دون تغيير سطر واحد من كود التطبيق.

المفاهيم الأساسية التي تعلمتها:

  • التوجيه القائم على الملفات يربط مسارات نظام الملفات بنقاط نهاية API
  • أدوات H3 توفر معالجة طلبات/استجابات محددة النوع
  • طبقة التخزين تجرّد خلفية قاعدة البيانات
  • قواعد المسارات تضبط التخزين المؤقت والرؤوس بشكل تصريحي
  • إعدادات النشر المسبقة تجمّع كودك لأي بيئة تشغيل

Nitro مُثبت في الإنتاج — يخدم ملايين الطلبات يومياً كمحرك وراء تطبيقات Nuxt حول العالم. سواء كنت تبني خدمة مصغّرة أو واجهة برمجة تطبيقات REST أو الخلفية لتطبيق متكامل، Nitro يضمن أن كود خادمك قابل للنقل وعالي الأداء ومستعد للمستقبل.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على الكشف عن أمراض أوراق النبات وتحديدها باستخدام YOLOv4.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة

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

تعلم كيفية بناء واجهة GraphQL API آمنة الأنواع بالكامل باستخدام Next.js 15 App Router و GraphQL Yoga و Pothos schema builder. يغطي هذا الدليل العملي تصميم المخططات والاستعلامات والتحولات والمصادقة وعميل React باستخدام urql.

30 د قراءة·