بناء واجهات برمجة تطبيقات آمنة الأنواع من البداية للنهاية مع tRPC و Next.js App Router

AI Bot
بواسطة AI Bot ·

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

أمان الأنواع من البداية للنهاية بدون توليد أكواد. يتيح لك tRPC استدعاء دوال الخادم من العميل مع إكمال تلقائي كامل لـ TypeScript، والتحقق من الصحة، ومعالجة الأخطاء — بدون مخططات REST، بدون محللات GraphQL، بدون مواصفات OpenAPI. في هذا الدليل، ستبني مدير مهام كامل باستخدام Next.js 15 App Router و tRPC.

ما ستتعلمه

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

  • إعداد tRPC v11 مع Next.js 15 App Router باستخدام محول fetch
  • تعريف الاستعلامات والتعديلات والاشتراكات مع التحقق بواسطة Zod
  • إنشاء وسيط (middleware) للمصادقة والتسجيل
  • دمج TanStack React Query v5 لجلب البيانات من جانب العميل
  • استخدام المستدعيات من جانب الخادم في Server Components
  • معالجة الأخطاء بنظام الأخطاء المدمج في tRPC
  • بناء مدير مهام يعمل بالكامل مع عمليات CRUD كاملة

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

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

  • Node.js 20+ مثبت (node --version)
  • خبرة في TypeScript (الأنواع، الأنواع العامة، الاستنتاج)
  • إلمام بـ Next.js App Router (Server Components، معالجات المسارات)
  • أساسيات React Query (اختياري، سنغطي ما تحتاجه)
  • محرر أكواد — يُنصح بـ VS Code أو Cursor

لماذا tRPC؟

إذا كان الواجهة الأمامية والخلفية كلاهما بلغة TypeScript، فأنتم بالفعل تتشاركون نظام أنواع واحد. فلماذا تكتب نقاط نهاية REST بأنواع طلب/استجابة منفصلة، أو تحافظ على مخطط GraphQL؟ يقضي tRPC على هذا التكرار بالكامل.

الميزةRESTGraphQLtRPC
أمان الأنواعيدويتوليد أكوادتلقائي
تعريف المخططOpenAPISDLغير مطلوب
منحنى التعلممنخفضمتوسطمنخفض
حجم الحزمةمتغيرثقيلأدنى حد
الأفضل لـAPIs عامةرسوم بيانية معقدةتطبيقات TS إلى TS

يتألق tRPC عندما يتشارك العميل والخادم نفس قاعدة أكواد TypeScript — وهذا بالضبط ما يوفره لك Next.js.


الخطوة 1: إنشاء مشروع Next.js

ابدأ بإنشاء هيكل مشروع Next.js 15 جديد:

npx create-next-app@latest trpc-task-manager --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd trpc-task-manager

الخطوة 2: تثبيت tRPC والتبعيات

ثبّت حزم tRPC مع TanStack React Query و Zod:

npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod

إليك ما تفعله كل حزمة:

  • @trpc/server — يعرّف موجه API والإجراءات والوسيط
  • @trpc/client — عميل TypeScript بسيط لاستدعاء API
  • @trpc/react-query — خطافات React تغلف TanStack React Query
  • @tanstack/react-query — إدارة حالة غير متزامنة قوية لـ React
  • zod — التحقق من المخطط وقت التشغيل واستنتاج أنواع TypeScript

الخطوة 3: تهيئة الواجهة الخلفية لـ tRPC

أنشئ ملف تهيئة tRPC. هنا تعرّف السياق وبناة الإجراءات.

أنشئ src/trpc/init.ts:

import { initTRPC, TRPCError } from "@trpc/server";
import { ZodError } from "zod";
 
// Define the context available to all procedures
export type Context = {
  userId: string | null;
};
 
// Create context for each request
export const createTRPCContext = async (): Promise<Context> => {
  // In a real app, extract user from session/JWT here
  return {
    userId: null,
  };
};
 
// Initialize tRPC — this should only be done once
const t = initTRPC.context<Context>().create({
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
});
 
// Export reusable helpers
export const router = t.router;
export const publicProcedure = t.procedure;
export const createCallerFactory = t.createCallerFactory;
 
// Middleware: require authentication
const enforceAuth = t.middleware(({ ctx, next }) => {
  if (!ctx.userId) {
    throw new TRPCError({
      code: "UNAUTHORIZED",
      message: "You must be logged in to perform this action",
    });
  }
  return next({
    ctx: {
      userId: ctx.userId,
    },
  });
});
 
export const protectedProcedure = t.procedure.use(enforceAuth);

يقوم هذا الملف بإعداد ثلاثة عناصر مهمة:

  1. السياق — البيانات المتاحة لكل إجراء (مثل المستخدم الحالي)
  2. الإجراءات العامة — يمكن الوصول إليها بدون مصادقة
  3. الإجراءات المحمية — تتطلب مستخدماً مسجل الدخول

الخطوة 4: تعريف موجه المهام

الآن أنشئ منطق API الفعلي. أنشئ src/trpc/routers/task.ts:

import { z } from "zod";
import { router, publicProcedure, protectedProcedure } from "../init";
import { TRPCError } from "@trpc/server";
 
// In-memory store (replace with a real database in production)
interface Task {
  id: string;
  title: string;
  description: string;
  completed: boolean;
  createdAt: Date;
  updatedAt: Date;
}
 
const tasks: Task[] = [
  {
    id: "1",
    title: "Learn tRPC",
    description: "Build a type-safe API with tRPC and Next.js",
    completed: false,
    createdAt: new Date(),
    updatedAt: new Date(),
  },
];
 
// Input validation schemas
const createTaskSchema = z.object({
  title: z.string().min(1, "Title is required").max(100),
  description: z.string().max(500).default(""),
});
 
const updateTaskSchema = z.object({
  id: z.string(),
  title: z.string().min(1).max(100).optional(),
  description: z.string().max(500).optional(),
  completed: z.boolean().optional(),
});
 
export const taskRouter = router({
  // GET all tasks
  list: publicProcedure
    .input(
      z
        .object({
          filter: z.enum(["all", "active", "completed"]).default("all"),
        })
        .optional()
    )
    .query(({ input }) => {
      const filter = input?.filter ?? "all";
 
      if (filter === "active") return tasks.filter((t) => !t.completed);
      if (filter === "completed") return tasks.filter((t) => t.completed);
      return tasks;
    }),
 
  // GET single task by ID
  byId: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(({ input }) => {
      const task = tasks.find((t) => t.id === input.id);
      if (!task) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: `Task with ID ${input.id} not found`,
        });
      }
      return task;
    }),
 
  // CREATE a new task
  create: publicProcedure
    .input(createTaskSchema)
    .mutation(({ input }) => {
      const newTask: Task = {
        id: crypto.randomUUID(),
        title: input.title,
        description: input.description,
        completed: false,
        createdAt: new Date(),
        updatedAt: new Date(),
      };
      tasks.push(newTask);
      return newTask;
    }),
 
  // UPDATE an existing task
  update: publicProcedure
    .input(updateTaskSchema)
    .mutation(({ input }) => {
      const index = tasks.findIndex((t) => t.id === input.id);
      if (index === -1) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: `Task with ID ${input.id} not found`,
        });
      }
 
      const updated = {
        ...tasks[index],
        ...input,
        updatedAt: new Date(),
      };
      tasks[index] = updated;
      return updated;
    }),
 
  // DELETE a task
  delete: publicProcedure
    .input(z.object({ id: z.string() }))
    .mutation(({ input }) => {
      const index = tasks.findIndex((t) => t.id === input.id);
      if (index === -1) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: `Task with ID ${input.id} not found`,
        });
      }
      const deleted = tasks.splice(index, 1)[0];
      return deleted;
    }),
});

لاحظ كيف يتم التحقق من كل مدخل باستخدام Zod. إذا أرسل العميل بيانات غير صالحة، يعيد tRPC تلقائياً خطأ 400 مع رسائل تحقق مفصلة — ويكتشف TypeScript عدم التطابق وقت التجميع.


الخطوة 5: إنشاء الموجه الرئيسي

اجمع كل موجهاتك في موجه جذر واحد. أنشئ src/trpc/routers/_app.ts:

import { router } from "../init";
import { taskRouter } from "./task";
 
export const appRouter = router({
  task: taskRouter,
});
 
// Export the type — this is the magic that enables end-to-end type safety
export type AppRouter = typeof appRouter;

نوع AppRouter هو المفتاح. تقوم بتصدير هذا النوع واستيراده في العميل — لا يعبر أي كود تشغيل الحدود، فقط الأنواع. يستنتج TypeScript كل مدخل ومخرج وخطأ من إجراءاتك.


الخطوة 6: إعداد معالج المسار

اربط tRPC بـ Next.js باستخدام محول fetch. أنشئ src/app/api/trpc/[trpc]/route.ts:

import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { createTRPCContext } from "@/trpc/init";
import { appRouter } from "@/trpc/routers/_app";
 
const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: createTRPCContext,
  });
 
export { handler as GET, handler as POST };

هذا ينشئ مسار شامل في /api/trpc/*. كل إجراء tRPC يصبح تلقائياً نقطة نهاية — task.list يتوافق مع /api/trpc/task.list.


الخطوة 7: إنشاء عميل tRPC

الآن قم بإعداد التكامل من جانب العميل. تحتاج ملفين.

أولاً، أنشئ خطافات tRPC React في src/trpc/client.ts:

import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "./routers/_app";
 
export const trpc = createTRPCReact<AppRouter>();

ثم أنشئ المزود في src/trpc/provider.tsx:

"use client";
 
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import { useState } from "react";
import { trpc } from "./client";
 
function getBaseUrl() {
  if (typeof window !== "undefined") return "";
  if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
  return `http://localhost:${process.env.PORT ?? 3000}`;
}
 
export function TRPCProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 5 * 1000,
        retry: 1,
      },
    },
  }));
 
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: `${getBaseUrl()}/api/trpc`,
        }),
      ],
    })
  );
 
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}

أضف المزود إلى layout الجذر في src/app/layout.tsx:

import { TRPCProvider } from "@/trpc/provider";
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <TRPCProvider>{children}</TRPCProvider>
      </body>
    </html>
  );
}

الخطوة 8: بناء واجهة مدير المهام

الآن الجزء الممتع — استخدام tRPC في مكوناتك مع أمان أنواع كامل.

أنشئ src/app/page.tsx:

"use client";
 
import { useState } from "react";
import { trpc } from "@/trpc/client";
 
export default function TaskManager() {
  const [filter, setFilter] = useState<"all" | "active" | "completed">("all");
  const [newTitle, setNewTitle] = useState("");
  const [newDescription, setNewDescription] = useState("");
 
  // Queries — fully typed, no manual type annotations needed
  const tasksQuery = trpc.task.list.useQuery({ filter });
 
  // Mutations with automatic cache invalidation
  const utils = trpc.useUtils();
 
  const createTask = trpc.task.create.useMutation({
    onSuccess: () => {
      utils.task.list.invalidate();
      setNewTitle("");
      setNewDescription("");
    },
  });
 
  const updateTask = trpc.task.update.useMutation({
    onSuccess: () => utils.task.list.invalidate(),
  });
 
  const deleteTask = trpc.task.delete.useMutation({
    onSuccess: () => utils.task.list.invalidate(),
  });
 
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!newTitle.trim()) return;
    createTask.mutate({
      title: newTitle,
      description: newDescription,
    });
  };
 
  return (
    <main className="max-w-2xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-8">Task Manager</h1>
 
      {/* Create Task Form */}
      <form onSubmit={handleSubmit} className="mb-8 space-y-4">
        <input
          type="text"
          value={newTitle}
          onChange={(e) => setNewTitle(e.target.value)}
          placeholder="Task title..."
          className="w-full p-3 border rounded-lg"
        />
        <textarea
          value={newDescription}
          onChange={(e) => setNewDescription(e.target.value)}
          placeholder="Description (optional)"
          className="w-full p-3 border rounded-lg"
          rows={2}
        />
        <button
          type="submit"
          disabled={createTask.isPending}
          className="px-6 py-3 bg-blue-600 text-white rounded-lg
                     hover:bg-blue-700 disabled:opacity-50"
        >
          {createTask.isPending ? "Adding..." : "Add Task"}
        </button>
      </form>
 
      {/* Filter Tabs */}
      <div className="flex gap-2 mb-6">
        {(["all", "active", "completed"] as const).map((f) => (
          <button
            key={f}
            onClick={() => setFilter(f)}
            className={`px-4 py-2 rounded-lg capitalize ${
              filter === f
                ? "bg-blue-600 text-white"
                : "bg-gray-100 hover:bg-gray-200"
            }`}
          >
            {f}
          </button>
        ))}
      </div>
 
      {/* Task List */}
      {tasksQuery.isLoading && <p>Loading tasks...</p>}
      {tasksQuery.error && (
        <p className="text-red-600">Error: {tasksQuery.error.message}</p>
      )}
 
      <ul className="space-y-3">
        {tasksQuery.data?.map((task) => (
          <li
            key={task.id}
            className="flex items-center gap-4 p-4 border rounded-lg"
          >
            <input
              type="checkbox"
              checked={task.completed}
              onChange={() =>
                updateTask.mutate({
                  id: task.id,
                  completed: !task.completed,
                })
              }
              className="w-5 h-5"
            />
            <div className="flex-1">
              <h3
                className={`font-medium ${
                  task.completed ? "line-through text-gray-400" : ""
                }`}
              >
                {task.title}
              </h3>
              {task.description && (
                <p className="text-sm text-gray-500">{task.description}</p>
              )}
            </div>
            <button
              onClick={() => deleteTask.mutate({ id: task.id })}
              className="text-red-500 hover:text-red-700"
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
 
      {tasksQuery.data?.length === 0 && (
        <p className="text-center text-gray-500 py-8">
          No tasks found. Create one above.
        </p>
      )}
    </main>
  );
}

لاحظ الإكمال التلقائي. عندما تكتب trpc.task.، يعرض محررك list، byId، create، update، delete. عندما تكتب createTask.mutate({، تحصل على إكمال تلقائي لـ title و description بأنواعهما الدقيقة. هذا هو سحر tRPC — صفر تعريفات أنواع يدوية على العميل.


الخطوة 9: استدعاءات من جانب الخادم في Server Components

من أفضل ميزات tRPC هي استدعاء الإجراءات مباشرة من Server Components بدون حمل HTTP زائد.

أنشئ src/trpc/server.ts:

import { createCallerFactory } from "./init";
import { appRouter } from "./routers/_app";
 
const createCaller = createCallerFactory(appRouter);
 
export const serverTRPC = createCaller({
  userId: null, // populate from session in real apps
});

الآن استخدمه في Server Component. أنشئ src/app/stats/page.tsx:

import { serverTRPC } from "@/trpc/server";
 
export default async function StatsPage() {
  // Direct function call — no HTTP, no serialization overhead
  const allTasks = await serverTRPC.task.list({ filter: "all" });
  const completedTasks = await serverTRPC.task.list({ filter: "completed" });
 
  const completionRate =
    allTasks.length > 0
      ? Math.round((completedTasks.length / allTasks.length) * 100)
      : 0;
 
  return (
    <main className="max-w-2xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-8">Task Statistics</h1>
      <div className="grid grid-cols-3 gap-4">
        <div className="p-6 bg-blue-50 rounded-lg text-center">
          <p className="text-3xl font-bold text-blue-600">{allTasks.length}</p>
          <p className="text-gray-600">Total Tasks</p>
        </div>
        <div className="p-6 bg-green-50 rounded-lg text-center">
          <p className="text-3xl font-bold text-green-600">
            {completedTasks.length}
          </p>
          <p className="text-gray-600">Completed</p>
        </div>
        <div className="p-6 bg-purple-50 rounded-lg text-center">
          <p className="text-3xl font-bold text-purple-600">
            {completionRate}%
          </p>
          <p className="text-gray-600">Completion Rate</p>
        </div>
      </div>
    </main>
  );
}

استدعاء serverTRPC.task.list() يعمل مباشرة على الخادم — نفس العملية، نفس الذاكرة، بدون قفزة شبكية. لا يزال TypeScript يفرض العقد الكامل.


الخطوة 10: إضافة وسيط التسجيل

يتيح لك وسيط tRPC إضافة اهتمامات عابرة مثل التسجيل وتحديد المعدل والتحليلات.

حدّث src/trpc/init.ts لإضافة وسيط تسجيل:

// Add this after the existing code in init.ts
 
const logger = t.middleware(async ({ path, type, next }) => {
  const start = Date.now();
  const result = await next();
  const duration = Date.now() - start;
 
  if (result.ok) {
    console.log(`[tRPC] ${type} ${path} — ${duration}ms OK`);
  } else {
    console.error(`[tRPC] ${type} ${path} — ${duration}ms ERROR`);
  }
 
  return result;
});
 
export const loggedProcedure = t.procedure.use(logger);

يمكنك تكديس الوسائط. يمكن للإجراء استخدام كل من logger و enforceAuth:

export const loggedProtectedProcedure = t.procedure
  .use(logger)
  .use(enforceAuth);

الخطوة 11: أفضل ممارسات معالجة الأخطاء

يوفر tRPC معالجة أخطاء منظمة مبنية مسبقاً. إليك كيفية استخدامها بفعالية.

رمي الأخطاء في الإجراءات

import { TRPCError } from "@trpc/server";
 
// In your procedure
throw new TRPCError({
  code: "BAD_REQUEST",
  message: "Title cannot be empty",
  cause: originalError, // optional — for debugging
});

رموز الأخطاء المتاحة

الرمزحالة HTTPمتى تستخدم
BAD_REQUEST400مدخل غير صالح
UNAUTHORIZED401غير مسجل الدخول
FORBIDDEN403لا يوجد إذن
NOT_FOUND404مورد مفقود
CONFLICT409إدخال مكرر
TOO_MANY_REQUESTS429تحديد المعدل
INTERNAL_SERVER_ERROR500خطأ غير متوقع

معالجة الأخطاء على العميل

const createTask = trpc.task.create.useMutation({
  onError: (error) => {
    // Zod validation errors
    if (error.data?.zodError) {
      const fieldErrors = error.data.zodError.fieldErrors;
      console.log("Validation errors:", fieldErrors);
      return;
    }
    // tRPC errors
    console.log("Error code:", error.data?.code);
    console.log("Message:", error.message);
  },
});

الخطوة 12: التحديثات التفاؤلية

لواجهة سريعة الاستجابة، يمكنك تحديث الذاكرة المؤقتة قبل أن يستجيب الخادم:

const updateTask = trpc.task.update.useMutation({
  onMutate: async (newData) => {
    // Cancel outgoing refetches
    await utils.task.list.cancel();
 
    // Snapshot current data
    const previous = utils.task.list.getData({ filter: "all" });
 
    // Optimistically update
    utils.task.list.setData({ filter: "all" }, (old) =>
      old?.map((task) =>
        task.id === newData.id ? { ...task, ...newData } : task
      )
    );
 
    return { previous };
  },
  onError: (_err, _newData, context) => {
    // Rollback on error
    if (context?.previous) {
      utils.task.list.setData({ filter: "all" }, context.previous);
    }
  },
  onSettled: () => {
    utils.task.list.invalidate();
  },
});

اختبار التطبيق

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

npm run dev

افتح http://localhost:3000 وتحقق من:

  1. تحميل قائمة المهام مع المهمة الأولية
  2. إمكانية إنشاء مهام جديدة من النموذج
  3. النقر على مربع الاختيار يبدّل حالة الإكمال
  4. زر الحذف يزيل المهام
  5. تبويبات الفلتر تعمل بشكل صحيح
  6. زيارة /stats لرؤية الإحصائيات المعروضة من جانب الخادم

الاختبار باستخدام curl

يمكنك أيضاً اختبار API مباشرة:

# List all tasks
curl "http://localhost:3000/api/trpc/task.list?input=%7B%7D"
 
# Create a task
curl -X POST "http://localhost:3000/api/trpc/task.create" \
  -H "Content-Type: application/json" \
  -d '{"json":{"title":"Test from curl","description":"Works!"}}'

هيكل المشروع

إليك هيكل المشروع النهائي:

src/
├── app/
│   ├── api/trpc/[trpc]/
│   │   └── route.ts          # معالج مسار tRPC
│   ├── stats/
│   │   └── page.tsx           # Server Component مع استدعاءات من جانب الخادم
│   ├── layout.tsx             # Layout الجذر مع TRPCProvider
│   └── page.tsx               # واجهة مدير المهام
└── trpc/
    ├── client.ts              # خطافات React (createTRPCReact)
    ├── init.ts                # تهيئة tRPC، السياق، الوسيط
    ├── provider.tsx           # مزود جانب العميل
    ├── server.ts              # مستدعي جانب الخادم
    └── routers/
        ├── _app.ts            # الموجه الجذر
        └── task.ts            # إجراءات المهام

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

أخطاء "Cannot find module"

تأكد أن tsconfig.json يحتوي على أسماء المسارات المستعارة:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

تحذيرات "Hydration mismatch"

تأكد أن TRPCProvider مُعلّم بـ "use client" ويغلف فقط أجزاء تطبيقك التي تحتاج خطافات tRPC من جانب العميل.

بيانات قديمة بعد التعديلات

استدعِ دائماً utils.task.list.invalidate() في onSuccess أو onSettled لإعادة جلب البيانات بعد التعديل.

أخطاء الأنواع بعد تغيير الإجراءات

إذا غيّرت مدخل/مخرج إجراء، قد يخزن TypeScript أنواعاً قديمة مؤقتاً. أعد تشغيل خادم TypeScript (Cmd+Shift+P → "TypeScript: Restart TS Server" في VS Code).


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

الآن بعد أن لديك إعداد tRPC يعمل، فكّر في هذه التحسينات:

  • إضافة قاعدة بيانات حقيقية — استبدل التخزين في الذاكرة بـ Drizzle ORM و PostgreSQL
  • إضافة المصادقة — ادمج AuthJS v5 واملأ ctx.userId
  • إضافة تحديثات في الوقت الحقيقي — استخدم اشتراكات tRPC مع WebSockets
  • إضافة الاختبارات — استخدم createCallerFactory لاختبار الإجراءات الوحدوية
  • النشر — احزمه بـ Docker أو انشره على Vercel

الخلاصة

يغيّر tRPC بشكل جذري طريقة بناء واجهات برمجة التطبيقات في تطبيقات TypeScript. بدلاً من الحفاظ على تعريفات أنواع منفصلة للعميل والخادم، تكتب إجراءاتك مرة واحدة وتدع TypeScript يستنتج كل شيء. النتيجة هي:

  • أخطاء أقل — يتم اكتشاف عدم تطابق الأنواع وقت التجميع، وليس في الإنتاج
  • تطوير أسرع — إكمال تلقائي لكل استدعاء API، بدون تعريفات أنواع يدوية
  • كود أقل — بدون نمط REST المتكرر، بدون محللات GraphQL، بدون خطوة توليد أكواد
  • تجربة مطور أفضل — أعد تسمية حقل على الخادم ويشير TypeScript فوراً لكل استخدام على العميل

مع Next.js App Router، تحصل على أفضل ما في العالمين: العرض من جانب الخادم مع استدعاءات خادم بدون حمل زائد، والتفاعلية من جانب العميل مع أمان أنواع كامل. هذه هي المنصة التي تجعل تطوير TypeScript الكامل سلساً حقاً.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على الدليل التفصيلي لتثبيت وهيكلة تطبيقك في Next.js لأداء أمثل.

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

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

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

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