بناء واجهات برمجة آمنة النوع باستخدام ORPC و Next.js 15 في 2026

إذا كنت تحب tRPC لكنك تتمنى لو أنه يدعم OpenAPI و REST بشكل أصلي ويتبع منهج العقد أولاً بطريقة أنظف، فإن ORPC هي المكتبة التي تحقق ذلك أخيرًا. في عام 2026 أصبحت الخيار المفضل بهدوء لدى الفرق التي تريد أمان النوع من طرف إلى طرف دون الارتباط بوسيط نقل أو إطار عمل واحد.
يرشدك هذا الدليل إلى بناء واجهة برمجية ORPC جاهزة للإنتاج داخل مشروع Next.js 15 باستخدام App Router. في النهاية ستحصل على إجراءات آمنة النوع، وعميل React مدعوم بـ TanStack Query، ووسيط مصادقة، ومعالجة أخطاء، وتوثيق OpenAPI مُنشأ تلقائيًا — كل ذلك في أقل من 30 دقيقة من العمل العملي.
المتطلبات الأساسية
قبل البدء، تأكد من توفر ما يلي:
- Node.js 20+ مثبت (يستخدم ORPC واجهات
ReadableStreamالحديثة) - Bun أو pnpm (الأمثلة تستخدم pnpm، لكن كلاهما يعمل)
- إلمام بـ TypeScript و React
- معرفة أساسية بـ Next.js App Router
- محرر أكواد يدعم TypeScript (يُنصح بـ VS Code)
ما الذي ستبنيه
واجهة إدارة مهام صغيرة تعرض:
task.list— قائمة مهام مع ترقيم الصفحاتtask.create— إنشاء مهمة مع التحقق عبر Zodtask.update— تحديث جزئي محمي بوسيطtask.delete— حذف مع فحص التفويض
سيتم استهلاك نفس الإجراءات بثلاث طرق مختلفة: من مكوّن React، ومن استدعاء fetch عادي يصل إلى واجهة REST، ومن عارض OpenAPI يُنشأ تلقائيًا بواسطة ORPC.
لماذا ORPC بدلاً من tRPC؟
يحتفظ ORPC (يُنطق "أو-آربيسي") بكل ما يحبه المطورون في tRPC — الأنواع المستنتجة، وسلاسل الوسائط، والأخطاء ذات النوع — ويضيف ثلاثة أمور كانت كافية لتبرير مكتبة جديدة:
- مخططات العقد أولًا. يمكنك تعريف عقد الإجراء في حزمة وتنفيذه في أخرى، وهو مثالي لمستودعات monorepo حيث تطبيق الجوال لم يعتمد Next.js بعد.
- نقل مزدوج. كل إجراء قابل للاستدعاء عبر RPC و REST معًا. يستخدم عملاء الويب نقل RPC الثنائي للسرعة، بينما تصل أطراف ثالثة إلى نقطة نهاية REST وتقرأ توثيق OpenAPI.
- استقلالية إطار العمل. نفس الموجّه يعمل على Next.js و Hono و Elysia و Bun و Cloudflare Workers أو خادم Node عادي. الترحيل لاحقًا يصبح تغييرًا في سطر واحد.
الخطوة 1: إنشاء مشروع Next.js
ابدأ من مشروع Next.js 15 جديد مع App Router و TypeScript:
pnpm create next-app@latest orpc-demo --typescript --app --tailwind --src-dir
cd orpc-demoأجب لا على ESLint إذا كنت تخطط لاستخدام Biome لاحقًا، ولا على Turbopack لتطابق التطوير مع الإنتاج.
الخطوة 2: تثبيت ORPC والحزم المرتبطة
ORPC مقسّم إلى حزم مركّزة حتى تدفع فقط مقابل ما تستورده.
pnpm add @orpc/server @orpc/client @orpc/contract @orpc/openapi
pnpm add @orpc/react-query @tanstack/react-query
pnpm add zodحزمة @orpc/contract اختيارية لكنها موصى بها — تتيح لك تعريف المخططات مرة واحدة وإعادة استخدامها على الخادم والعميل.
الخطوة 3: تعريف العقد
أنشئ ملفًا جديدًا يصف شكل كل إجراء. اعتبر ذلك بديلًا أصليًا لـ TypeScript عن ملف OpenAPI YAML.
// src/server/contract.ts
import { oc } from '@orpc/contract'
import { z } from 'zod'
const TaskSchema = z.object({
id: z.string().uuid(),
title: z.string().min(1).max(120),
done: z.boolean(),
createdAt: z.date(),
})
export const contract = oc.router({
task: {
list: oc
.route({ method: 'GET', path: '/tasks' })
.input(z.object({ cursor: z.string().optional(), limit: z.number().int().min(1).max(100).default(20) }))
.output(z.object({ items: z.array(TaskSchema), nextCursor: z.string().nullable() })),
create: oc
.route({ method: 'POST', path: '/tasks' })
.input(z.object({ title: z.string().min(1).max(120) }))
.output(TaskSchema),
update: oc
.route({ method: 'PATCH', path: '/tasks/{id}' })
.input(z.object({ id: z.string().uuid(), title: z.string().optional(), done: z.boolean().optional() }))
.output(TaskSchema),
delete: oc
.route({ method: 'DELETE', path: '/tasks/{id}' })
.input(z.object({ id: z.string().uuid() }))
.output(z.object({ success: z.literal(true) })),
},
})
export type Contract = typeof contractلاحظ أن كل إجراء يحصل على مسار REST. سيخدم ORPC صيغة RPC الثنائية و مسار REST من نفس الموجّه — بدون ازدواجية.
الخطوة 4: تنفيذ الموجّه
الآن اربط منطق الأعمال بالعقد. سنستخدم في هذا الدليل مخزنًا في الذاكرة، لكن استبدله بـ Prisma أو Drizzle أو Supabase في مشروعك الحقيقي.
// src/server/router.ts
import { implement } from '@orpc/server'
import { randomUUID } from 'node:crypto'
import { contract } from './contract'
type Task = {
id: string
title: string
done: boolean
createdAt: Date
}
const store = new Map<string, Task>()
const os = implement(contract)
const taskListHandler = os.task.list.handler(async ({ input }) => {
const items = Array.from(store.values()).slice(0, input.limit)
return { items, nextCursor: null }
})
const taskCreateHandler = os.task.create.handler(async ({ input }) => {
const task: Task = {
id: randomUUID(),
title: input.title,
done: false,
createdAt: new Date(),
}
store.set(task.id, task)
return task
})
const taskUpdateHandler = os.task.update.handler(async ({ input, errors }) => {
const existing = store.get(input.id)
if (!existing) throw errors.NOT_FOUND({ message: 'Task not found' })
const updated = { ...existing, ...input }
store.set(updated.id, updated)
return updated
})
const taskDeleteHandler = os.task.delete.handler(async ({ input, errors }) => {
if (!store.has(input.id)) throw errors.NOT_FOUND({ message: 'Task not found' })
store.delete(input.id)
return { success: true as const }
})
export const router = os.router({
task: {
list: taskListHandler,
create: taskCreateHandler,
update: taskUpdateHandler,
delete: taskDeleteHandler,
},
})
export type Router = typeof routerاستدعاء implement(contract) يضمن في وقت الترجمة أن كل معالج يطابق العقد — أعد تسمية حقل و سيُشير TypeScript فورًا إلى المعالج المكسور.
الخطوة 5: ربط ORPC بمعالج مسار Next.js
App Router يجعل هذا تافهًا. أنشئ مسارًا شاملًا يوجّه كل طلب إلى معالج ORPC.
// src/app/api/[[...rest]]/route.ts
import { RPCHandler } from '@orpc/server/fetch'
import { OpenAPIHandler } from '@orpc/openapi/fetch'
import { router } from '@/server/router'
const rpcHandler = new RPCHandler(router)
const openapiHandler = new OpenAPIHandler(router)
async function handle(request: Request) {
const url = new URL(request.url)
if (url.pathname.startsWith('/api/rpc')) {
const { response } = await rpcHandler.handle(request, { prefix: '/api/rpc' })
if (response) return response
}
const { response } = await openapiHandler.handle(request, { prefix: '/api' })
if (response) return response
return new Response('Not found', { status: 404 })
}
export const GET = handle
export const POST = handle
export const PATCH = handle
export const DELETE = handleالآن /api/rpc/task.list يخدم مكالمات RPC الثنائية و/api/tasks يخدم نفس البيانات تمامًا عبر REST. موجّه واحد، وسيلتا نقل.
الخطوة 6: بناء العميل
أنشئ مساعدًا من جانب العميل يعيد استخدام نفس نوع Router حتى يتم إكمال كل استدعاء تلقائيًا والتحقق من نوعه.
// src/lib/orpc-client.ts
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
import type { Router } from '@/server/router'
const link = new RPCLink({ url: '/api/rpc' })
export const orpc = createORPCClient<Router>(link)بفضل نوع Router المُصدَّر، فإن orpc.task.create({ title: 'Ship ORPC demo' }) مكتمل النوع — المدخلات والمخرجات والأخطاء المحتملة كلها مستنتجة.
الخطوة 7: ربط TanStack Query
يوفر ORPC ارتباطات TanStack Query من الدرجة الأولى. ثبّت المزوّد في جذر شجرة App Router.
// src/app/providers.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState, type ReactNode } from 'react'
export function Providers(props: { children: ReactNode }) {
const [client] = useState(() => new QueryClient())
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
}غلّف التخطيط الجذري:
// src/app/layout.tsx
import { Providers } from './providers'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ar" dir="rtl">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}الآن ابنِ مكوّن قائمة مهام يقرأ ويحوّر المهام:
// src/app/page.tsx
'use client'
import { createORPCReactQueryUtils } from '@orpc/react-query'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { orpc } from '@/lib/orpc-client'
const $orpc = createORPCReactQueryUtils(orpc)
export default function HomePage() {
const qc = useQueryClient()
const [title, setTitle] = useState('')
const tasks = useQuery($orpc.task.list.queryOptions({ input: { limit: 20 } }))
const createTask = useMutation({
...$orpc.task.create.mutationOptions(),
onSuccess: () => qc.invalidateQueries({ queryKey: $orpc.task.list.key() }),
})
return (
<main className="mx-auto max-w-xl p-8">
<h1 className="text-2xl font-bold">المهام</h1>
<form
onSubmit={(e) => {
e.preventDefault()
createTask.mutate({ title })
setTitle('')
}}
className="mt-4 flex gap-2"
>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
className="flex-1 rounded border px-3 py-2"
placeholder="ما الذي يجب إنجازه؟"
/>
<button className="rounded bg-black px-4 py-2 text-white">إضافة</button>
</form>
<ul className="mt-6 space-y-2">
{tasks.data?.items.map((task) => (
<li key={task.id} className="rounded border p-3">
{task.title}
</li>
))}
</ul>
</main>
)
}إذا أعدت تسمية title إلى name في العقد، سيُبرز TypeScript استدعاء الطفرة المكسور قبل حتى أن تحفظ الملف.
الخطوة 8: إضافة وسيط المصادقة
حماية المسارات هي المكان الذي يبدو فيه ORPC متقنًا بشكل خاص. يتكامل الوسيط مثل وسيط Express لكنه يحافظ على نوع الإرجاع مستنتجًا بالكامل.
// src/server/middleware.ts
import { os } from './router'
import { ORPCError } from '@orpc/server'
import { cookies } from 'next/headers'
export const authed = os.middleware(async ({ context, next }) => {
const session = (await cookies()).get('session')?.value
if (!session) throw new ORPCError('UNAUTHORIZED', { message: 'Sign in to continue' })
const user = { id: session, role: 'member' as const }
return next({ context: { ...context, user } })
})طبّقه على الإجراءات المُعدِّلة فقط:
const taskCreateHandler = os.task.create
.use(authed)
.handler(async ({ input, context }) => {
const task: Task = {
id: randomUUID(),
title: `${input.title} (by ${context.user.id})`,
done: false,
createdAt: new Date(),
}
store.set(task.id, task)
return task
})يحصل المستدعون الآن على خطأ UNAUTHORIZED مُصنَّف يمكنهم تضييقه باستخدام if (error.code === 'UNAUTHORIZED').
الخطوة 9: إنشاء توثيق OpenAPI تلقائيًا
يمكن لحزمة @orpc/openapi إصدار مواصفات من نفس العقد، مما يعني أن التوثيق لا يمكن أن يبتعد عن التنفيذ.
// src/app/api/openapi/route.ts
import { OpenAPIGenerator } from '@orpc/openapi'
import { ZodToJsonSchemaConverter } from '@orpc/zod'
import { contract } from '@/server/contract'
const generator = new OpenAPIGenerator({
schemaConverters: [new ZodToJsonSchemaConverter()],
})
export async function GET() {
const spec = await generator.generate(contract, {
info: { title: 'Tasks API', version: '1.0.0' },
servers: [{ url: '/api' }],
})
return Response.json(spec)
}اقرنه مع Scalar أو Swagger UI للحصول على صفحة توثيق جميلة على /api/openapi.
الخطوة 10: معالجة الأخطاء في العميل
لأن الأخطاء جزء من العقد، يتعامل معها العملاء دون تخمين:
import { isDefinedError } from '@orpc/client'
try {
await orpc.task.update({ id: 'bad-id', title: 'Nope' })
} catch (error) {
if (isDefinedError(error) && error.code === 'NOT_FOUND') {
console.warn('Task disappeared — refresh the list')
} else {
throw error
}
}لا مزيد من تحليل كائنات Error العامة أو الدعاء بأن رمز الحالة يطابق ما أرسله الخلفية يوم الجمعة الماضي.
اختبار التنفيذ
شغّل خادم التطوير وجرّبه من طرف إلى طرف:
pnpm devثم مارس كلا وسيلتي النقل:
curl http://localhost:3000/api/tasks
curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Finish ORPC tutorial"}'افتح http://localhost:3000 في متصفحك وأضف مهمة من النموذج. يجب أن تبطل ذاكرة التخزين المؤقت لـ React Query تلقائيًا وأن تظهر المهمة الجديدة في غمضة عين.
استكشاف الأخطاء وإصلاحها
"Cannot find module '@orpc/server/fetch'" — تأكد أنك على Node 20 أو أحدث. لا يوفر Node 18 العام ReadableStream الذي يستخدمه ORPC.
الأنواع هي any في العميل — تحقق من أن tsconfig.json يحتوي على "moduleResolution": "Bundler" وأن نوع Router مُصدَّر بـ export type وليس بـ export عادي.
أخطاء CORS في الإنتاج — لا يضبط ORPC رؤوس CORS. استخدم middleware.ts في Next.js أو معالجًا لكل مسار لإضافتها عندما تكون نقطة نهاية RPC على أصل مختلف عن العميل.
OpenAPI لا يُحدَّث بعد تغيير المخطط — يُنشأ المواصفات في وقت الطلب، لذا إعادة التحميل القسرية كافية. إذا خزّنت الاستجابة، تذكّر إبطال التخزين المؤقت.
الخطوات التالية
- استبدل المخزن في الذاكرة بـ Prisma باستخدام دليل Prisma لـ Next.js.
- أضف تحديثات متفائلة باستخدام استدعاء
onMutateالخاص بـ TanStack Query. - انشر على Cloudflare Workers — نفس الموجّه يعمل دون تغيير بفضل محوّل Fetch.
- أنشئ SDK مُصنَّف لتطبيق الجوال الخاص بك بإعادة تصدير العقد من حزمة مشتركة.
- أضف طبقة تحديد المعدل باستخدام Arcjet أو Upstash.
الخاتمة
ORPC في Next.js 15 يصيب نقطة مثالية يفوتها tRPC و REST و GraphQL العادي على حدة. تحصل على أمان النوع مدفوعًا بالعقد عبر الشبكة، وواجهة OpenAPI من الدرجة الأولى للمستهلكين الخارجيين، ووقت تشغيل يتبعك من Vercel إلى Cloudflare إلى Bun دون إعادة كتابة.
السحر الحقيقي هو الطريقة التي يصبح بها ملف واحد — عقدك — هو مخطط الإدخال، ونوع الإخراج، وتوقيع الوسيط، ومستند OpenAPI، وخطاف React Query. هذا المصدر الواحد للحقيقة هو ما يجعل الفرق أسرع كلما نمت قاعدة الشيفرة، وهو السبب في أن ORPC أصبح توصيتنا الافتراضية لواجهات Next.js البرمجية الجديدة في 2026.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

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

Medusa.js 2.0 — بناء متجر إلكتروني Headless مع Next.js (2026)
تعلم كيفية بناء متجر إلكتروني كامل باستخدام Medusa.js 2.0 و Next.js. من كتالوج المنتجات إلى الدفع، هذا الدليل يغطي كل شيء بـ TypeScript.

بناء نظام بريد إلكتروني للمعاملات باستخدام Resend و React Email في Next.js
تعلم كيفية بناء رسائل بريد إلكتروني جميلة وآمنة النوع للمعاملات باستخدام React Email و Resend في تطبيق Next.js. يغطي هذا الدرس تصميم قوالب البريد الإلكتروني وسير عمل المعاينة والإرسال عبر مسارات API والنشر في بيئة الإنتاج.