tRPC v11 أمان الأنواع من الطرف للطرف: Server +
٢٢ يونيو ٢٠٢٦
تمنح tRPC عميل TypeScript استنتاجاً كاملاً للأنواع (type inference) عبر إجراءات الخادم بدون توليد كود (code generation) وبدون مخطط API منفصل. يبني هذا البرنامج التعليمي API كاملاً وقابلاً للتشغيل باستخدام tRPC v11 — خادم Node مستقل وعميل vanilla — بدون Next.js أو أي إطار عمل.
ملخص
ستقوم ببناء API صغير لـ "الملاحظات" من مجلد فارغ باستخدام tRPC v11.18.0. ستحدد راوتر (router) مع استعلامين (queries) وطفرة (mutation)، وتتحقق من المدخلات باستخدام Zod، وتخدمها باستخدام المحول المستقل (غلاف رفيع فوق خادم http الخاص بـ Node)، ثم تستدعيها من عميل vanilla مكتوب بالكامل حيث يعرف المحرر كل نوع إدخال وإرجاع. ستضيف معالجة TRPCError، وتفحص تنسيق HTTP الخام باستخدام curl، وتصلح خطأ إعداد شائعاً جداً (عدم تطابق بين basePath و url). تم التحقق من كل أمر وكتلة كود من البداية إلى النهاية — tsc --noEmit، والخادم، والعميل، و curl — في 22 يونيو 2026.
ما ستتعلمه
- كيفية هيكلة مشروع Node + TypeScript يقوم بتشغيل ملفات
.tsمباشرة - كيفية تعريف راوتر tRPC مع استعلامات وطفرة
- كيفية التحقق من صحة مدخلات الإجراء باستخدام Zod
- كيفية خدمة الراوتر باستخدام المحول المستقل وتمكين CORS
- كيفية بناء عميل vanilla تأتي أنواعه مباشرة من الخادم
- كيفية رمي ومعالجة الأخطاء المكتوبة باستخدام
TRPCError - كيفية التحقق من API عبر HTTP الخام باستخدام
curl
المتطلبات الأساسية
- Node 20 أو أحدث — أي إصدار LTS مدعوم سيعمل. تم التحقق هنا على Node 22.22.3. (لا تعلن tRPC v11 عن محرك Node محدد، لكن المحول المستقل يغلف خادم HTTP المدمج في Node، لذا فإن إصدار LTS الحالي هو الأساس الآمن.)
- TypeScript 5.7.2 أو أحدث — هذا هو متطلب tRPC v11.1 لقد ثبتنا الإصدار 6.0.3.
- إلمام أساسي بـ
async/awaitوالطرفية (terminal).
ملاحظة سريعة حول الإصدارات: تم إطلاق tRPC v11 كإصدار مستقر في 21 مارس 20252 بعد فترة طويلة تحت وسم @next، لذا فإن API في هذا الدليل مستقر وليس تجريبياً.
الخطوة 1 — هيكلة المشروع
أنشئ مجلداً، وقم بتهيئة حزمة ESM، وثبت التبعيات المحددة.
mkdir trpc-notes && cd trpc-notes
npm init -y
npm pkg set type=module
# runtime deps
npm install @trpc/server@11.18.0 @trpc/client@11.18.0 zod@4.4.3 cors@2.8.6
# dev deps: TypeScript, a .ts runner, and types
npm install -D TypeScript@6.0.3 tsx@4.22.4 @types/node@26.0.0 @types/cors@2.8.19
يسمح لنا tsx بتشغيل ملفات TypeScript مباشرة (بدون خطوة بناء منفصلة أثناء التطوير)، و cors مطلوب فقط لأن الخادم المستقل لا يضبط ترويسات CORS من تلقاء نفسه.3
أنشئ ملف tsconfig.json مهيأً لفحص الأنواع (وليس لإصدار الكود — حيث يتولى tsx التنفيذ):
{
"compilerOptions": {
"target": "es2022",
"module": "preserve",
"moduleResolution": "bundler",
"strict": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"]
}
تجبرك خاصية verbatimModuleSyntax على استخدام import type لاستيراد الأنواع فقط — وهو أمر يهم كثيراً بعد قليل، لأن استيراد نوع الراوتر الخاص بك هو الحيلة الكاملة التي تبقي كود الخادم بعيداً عن حزمة العميل (client bundle).
الخطوة 2 — تعريف راوتر tRPC مع استعلامات وطفرة
هذا هو جوهر tRPC. أنشئ src/router.ts. ستقوم بتهيئة tRPC مرة واحدة، وتصدير مساعدين قابلين لإعادة الاستخدام، ثم تعلن عن الإجراءات. الـ .query() مخصص للقراءة، والـ .mutation() مخصص للكتابة.
// src/router.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
interface Note {
id: string;
title: string;
body: string;
createdAt: string;
}
// In a real app this would be your database. Keep it in memory for the demo.
const notes: Note[] = [
{ id: '1', title: 'Welcome', body: 'Your first note.', createdAt: new Date().toISOString() },
];
export const appRouter = router({
// a query with no input
list: publicProcedure.query(() => notes),
// a query with a validated string input
byId: publicProcedure.input(z.string()).query(({ input }) => {
const note = notes.find((n) => n.id === input);
if (!note) {
throw new TRPCError({ code: 'NOT_FOUND', message: `No note with id ${input}` });
}
return note;
}),
// a mutation with a validated object input
create: publicProcedure
.input(z.object({ title: z.string().min(1), body: z.string().min(1) }))
.mutation(({ input }) => {
const note: Note = {
id: String(notes.length + 1),
title: input.title,
body: input.body,
createdAt: new Date().toISOString(),
};
notes.push(note);
return note;
}),
});
// Export ONLY the type. This is what the client imports.
export type AppRouter = typeof appRouter;
السطر الأخير هو الذي يمنح tRPC شعارها المميز. AppRouter هو نوع TypeScript نقي يصف كل إجراء، ومدخلاته، وقيمة إرجاعه. يستورد العميل هذا النوع ولا شيء غيره — لا يعبر أي كود خادم وقت التشغيل الحدود.
الخطوة 3 — التحقق من صحة المدخلات باستخدام Zod
لقد قمت بذلك بالفعل في الخطوة 2، ويستحق الأمر التمهل عنده لأنه يؤدي وظيفة مزدوجة. تأخذ كل مكالمة .input() مدققاً (validator). في وقت التشغيل، تقوم tRPC بتحليل الطلب الوارد مقابل المخطط وترفض أي شيء لا يتطابق قبل تشغيل المعالج الخاص بك. في وقت الترجمة (compile time)، يصبح النوع المستنتج من المخطط هو نوع input داخل المعالج — والنوع الذي يضطر العميل لتمريره.
لذا فإن z.object({ title: z.string().min(1), body: z.string().min(1) }) يعني أن استدعاء العميل لـ create مع عنوان مفقود، أو رقم حيث يجب أن يكون هناك نص، هو خطأ TypeScript في المحرر الخاص بك و خطأ تحقق في وقت التشغيل. تقبل tRPC v11 أي مدقق ينفذ مواصفات Standard Schema، لذا فإن Zod و Valibot و ArkType جميعها تعمل بنفس الطريقة.4 نحن نستخدم Zod 4 هنا.
الخطوة 4 — خدمتها باستخدام المحول المستقل
المحول المستقل هو أبسط طريقة لتشغيل راوتر tRPC: فهو غلاف رفيع حول خادم HTTP المدمج في Node.3 أنشئ src/server.ts:
// src/server.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import cors from 'cors';
import { appRouter } from './router';
const server = createHTTPServer({
router: appRouter,
middleware: cors(), // allow browser clients during development
basePath: '/trpc/', // requests are served under /trpc/*
createContext() {
return {};
},
});
server.listen(3000);
console.log('tRPC server on http://localhost:3000/trpc');
هناك تفصيلان يقع فيهما الناس عادةً. أولاً، يتم تمرير cors() كـ middleware لأن الخادم المستقل لا يضبط أي ترويسات CORS افتراضياً — بدونها، ستفشل عملاء المتصفح بخطأ CORS (لا تتأثر استدعاءات خادم إلى خادم و curl).3 ثانياً، basePath: '/trpc/' تعني أن كل إجراء يعيش تحت /trpc/ — يصبح list هو http://localhost:3000/trpc/list. الـ basePath الافتراضي هو '/'؛ لقد قمنا بضبطه صراحةً ليتطابق مع رابط العميل في الخطوة التالية. تذكر هذا — فهو الخطأ المذكور في قسم استكشاف الأخطاء وإصلاحها.
ابدأ تشغيله في طرفية واحدة:
npx tsx src/server.ts
# tRPC server on http://localhost:3000/trpc
الخطوة 5 — بناء عميل vanilla تأتي أنواعه من الخادم
أنشئ src/client.ts. يستورد العميل نوع AppRouter (لاحظ import type) ويمرره إلى createTRPCClient. هذا النوع العام (generic) الوحيد هو ما يجعل العميل بأكمله آمناً من حيث الأنواع.
// src/client.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './router';
const trpc = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })],
});
async function main() {
// `query` for reads, `mutate` for writes — both fully typed
console.log('list ->', await trpc.list.query());
const created = await trpc.create.mutate({ title: 'Buy milk', body: '2 liters' });
console.log('create ->', created);
console.log('byId ->', await trpc.byId.query(created.id));
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
createTRPCClient هو الاسم في الإصدار v11. إذا كنت تتبع برنامجاً تعليمياً أقدم يستخدم createTRPCProxyClient، فإن هذه الوظيفة لا تزال موجودة ولكنها مهجورة — حيث أسقط الإصدار v11 كلمة "Proxy" من عميل API العام.1 يقوم رابط httpBatchLink النهائي بتجميع مكالمات متزامنة متعددة في طلب HTTP واحد تلقائياً.5
قم بتشغيله في طرفية ثانية (اترك الخادم قيد التشغيل):
npx tsx src/client.ts
سترى المخرجات الموضحة أدناه — يعيد byId الملاحظة التي أنشأتها للتو، لأن المخزن في الذاكرة يستمر طوال فترة تشغيل عملية الخادم:
list -> [
{ id: '1', title: 'Welcome', body: 'Your first note.', createdAt: '2026-06-22T03:15:07.507Z' }
]
create -> { id: '2', title: 'Buy milk', body: '2 liters', createdAt: '2026-06-22T03:15:09.537Z' }
byId -> { id: '2', title: 'Buy milk', body: '2 liters', createdAt: '2026-06-22T03:15:09.537Z' }
الفائدة الكبرى: مرر الفأرة فوق created في المحرر الخاص بك وستجده من النوع { id: string; title: string; body: string; createdAt: string } — مستنتج مباشرة من معالج الخادم، بدون واجهة مشتركة، ولا مستند OpenAPI، ولا خطوة توليد كود. قم بتغيير شكل الإرجاع في الخادم وسيتوقف العميل عن الترجمة فوراً. هذا هو معنى "أمان الأنواع من البداية إلى النهاية" في الممارسة العملية.
الخطوة 6 — معالجة الأخطاء باستخدام TRPCError
لقد قمت بالفعل برمي TRPCError في byId. يقوم tRPC بربط code الخاص بالخطأ بحالة HTTP تلقائيًا، لذا فإن NOT_FOUND تتحول إلى 404 دون أن تلمس أكواد الحالة بنفسك.6 أضف هذا إلى main() الخاص بالعميل لرؤيته يظهر كرفض حقيقي:
try {
await trpc.byId.query('999'); // no such note
} catch (err) {
// err is a TRPCClientError; err.data.code is 'NOT_FOUND', err.data.httpStatus is 404
console.error('expected error ->', (err as Error).message);
}
على جانب العميل، القيمة المرمية هي TRPCClientError تحمل رسالة الخادم بالإضافة إلى كائن data يحتوي على code و httpStatus. استخدم أكواد الخطأ الموثقة (BAD_REQUEST، UNAUTHORIZED، FORBIDDEN، NOT_FOUND، CONFLICT، INTERNAL_SERVER_ERROR، وغيرها) بدلاً من اختراع أكوادك الخاصة — فكل منها يرتبط بحالة HTTP محددة.6 ملاحظة للإنتاج: في بيئة التطوير، تتضمن حمولة الخطأ تتبع stack كاملاً؛ بينما يتجاهل tRPC المكدس تلقائيًا عندما يكون NODE_ENV هو production.6
التحقق — تأكد من أنه يعمل عبر HTTP الخام
إن tRPC هو "مجرد HTTP"، ويمكنك إثبات ذلك بدون العميل على الإطلاق. أعد تشغيل الخادم أولاً حتى يحتوي المخزن في الذاكرة على الملاحظة الأولية فقط، ثم اختبره باستخدام curl. تُرسل الاستعلامات (Queries) كـ GET والتحويلات (Mutations) كـ POST؛ يقوم httpBatchLink بتغليف كل شيء في غلاف دفعي (batch envelope)، لذا يتم تحديد المدخلات بواسطة الفهرس ("0") وتعود الاستجابات كمصفوفة.
# a query (GET): list all notes
curl 'http://localhost:3000/trpc/list?batch=1&input=%7B%7D'
# [{"result":{"data":[{"id":"1","title":"Welcome", ... }]}}]
# a query with input (GET): byId('1') — input is {"0":"1"} url-encoded
curl 'http://localhost:3000/trpc/byId?batch=1&input=%7B%220%22%3A%221%22%7D'
# [{"result":{"data":{"id":"1","title":"Welcome", ... }}}]
# a mutation (POST): create — body is {"0":{...}}; on a fresh server this is the 2nd note
curl -X POST 'http://localhost:3000/trpc/create?batch=1' \
-H 'content-type: application/json' \
-d '{"0":{"title":"From curl","body":"hello"}}'
# [{"result":{"data":{"id":"2","title":"From curl", ... }}}]
الملاحظة المفقودة تعيد غلاف خطأ محدد النوع، مع ضبط حالة HTTP على 404:
curl 'http://localhost:3000/trpc/byId?batch=1&input=%7B%220%22%3A%22999%22%7D'
# [{"error":{"message":"No note with id 999","code":-32004,
# "data":{"code":"NOT_FOUND","httpStatus":404,"path":"byId", ... }}}]
الكود -32004 هو كود خطأ JSON-RPC الذي يستخدمه tRPC لـ NOT_FOUND؛ أما data.code و data.httpStatus المقروءان بشريًا فهما ما تبرمج بناءً عليه فعليًا.
أخيرًا، قم بفحص الأنواع (type-check) للمشروع بأكمله للتأكد من عدم وجود أي شيء غير محدد النوع بدقة:
npx tsc --noEmit
# (no output = success)
الأخطاء الشائعة
- كل استدعاء للعميل يعيد 404، أو
Unable to transform response from server. هناك تعارض بينbasePathالخاص بالخادم وurlالخاص بالعميل. إذا ضبط الخادمbasePath: '/trpc/'، يجب أن يكونurlالخاص بالعميل هوhttp://localhost:3000/trpc(وليس المضيف المجرد). هذا التعارض هو خطأ شائع جدًا في التشغيل الأول لأن مثال الخادم المستقل الرسمي يفترض افتراضيًا أنbasePathهو'/'بينما يشير مثال العميل إلى/trpc— اختر واحدًا وحافظ على مزامنتهما.37 - يفشل عميل المتصفح مع خطأ CORS، لكن
curlيعمل. لقد نسيتmiddleware: cors()على الخادم المستقل. تتجاهلcurlواستدعاءات الخادم إلى الخادم CORS؛ لكن المتصفحات لا تفعل ذلك.3 - تحذير
createTRPCProxyClient is deprecated. قم بتغيير اسمه إلىcreateTRPCClient— نفس الخيارات، اسم جديد في الإصدار v11.1 - خطأ
tsc: تبعية نظيرة / TypeScript غير مدعوم. يتطلب tRPC v11 إصدار TypeScript ≥ 5.7.2.1 تحقق منnpx tsc --version. The transformer property has moved. في الإصدار v11، يتم تكوين محول البيانات (مثل superjson) على الرابط (httpBatchLink({ url, transformer }))، وليس في جذر العميل كما في الإصدار v10.1
الخطوات التالية
لديك الآن API آمن النوع بالكامل مع خادم وعميل وتحقق من الصحة ومعالجة الأخطاء — وقد رأيت أنه يعتمد على HTTP العادي في جوهره. من هنا:
- استبدل المصفوفة الموجودة في الذاكرة بقاعدة بيانات حقيقية. يوضح دليل Drizzle ORM + pg-boss Atomic Transactions Tutorial طبقة بيانات محددة النوع يمكن لتحويلاتك استدعاؤها.
- اعتمد بشكل أكبر على أنماط التحقق القائمة على المخطط (schema-first)، بنفس الطريقة التي يدفع بها دليل TanStack Router Type-Safe Search Params With Zod مكتبة Zod إلى أطراف التطبيق.
- سرّع فحص الأنواع على موجه (router) متنامٍ باستخدام المترجم الأصلي المذكور في TypeScript 7 (tsgo): 10x Faster Compiler.
للإنتاج، أضف createContext موثقًا، واستبدل المحول المستقل بمحول Express أو Fastify أو Fetch الذي يناسب مضيفك، وفكر في تكامل TanStack React Query عند ربط هذا بواجهة أمامية.
Footnotes
-
وثائق tRPC، "Migrate from v10 to v11" —
createTRPCProxyClient←createTRPCClient، transformer-on-link، TypeScript ≥ 5.7.2. https://trpc.io/docs/migrate-from-v10-to-v11 ↩ ↩2 ↩3 ↩4 ↩5 -
tRPC، "Announcing tRPC v11" (21 مارس 2025). https://trpc.io/blog/announcing-trpc-v11 ↩
-
وثائق tRPC، "Standalone Adapter" (v11)، بما في ذلك سلوك
createHTTPServer، وbasePath، و CORS/middleware. https://trpc.io/docs/server/adapters/standalone ↩ ↩2 ↩3 ↩4 ↩5 -
وثائق tRPC، "Input & Output Validators" — دعم Standard Schema (Zod، Valibot، ArkType). https://trpc.io/docs/server/validators ↩
-
وثائق tRPC، "HTTP Batch Link" — تجميع استدعاءات متعددة في طلب واحد. https://trpc.io/docs/client/links/httpBatchLink ↩
-
وثائق tRPC، "Error Handling" — أكواد
TRPCErrorوتعيينات حالات HTTP الخاصة بها. https://trpc.io/docs/server/error-handling ↩ ↩2 ↩3 -
وثائق tRPC، "Set up a tRPC Client" (v11) — استخدام
createTRPCClient، وhttpBatchLink، وclient.x.query()/.mutate(). https://trpc.io/docs/client/vanilla/setup ↩