tRPC چیست؟ آموزش ساخت API کاملاً Type-Safe با TypeScript، Node.js و React

در این آموزش tRPC را از صفر راه‌اندازی می‌کنیم و با TypeScript، Node.js، Express، React، Zod و TanStack Query یک API کاملاً Type-Safe برای برنامه هوش مصنوعی می‌سازیم.

Share
tRPC چیست؟ آموزش ساخت API کاملاً Type-Safe با TypeScript، Node.js و React


در یک برنامه Full-Stack معمولاً Backend و Frontend به‌صورت جداگانه توسعه داده می‌شوند. Backend یک API می‌سازد و Frontend باید دقیقاً بداند هر Endpoint چه ورودی‌هایی می‌پذیرد و چه خروجی‌ای برمی‌گرداند.

اگر Backend تغییر کند اما Typeهای Frontend به‌روزرسانی نشوند، مشکلاتی مانند این موارد ایجاد می‌شوند:

  • Frontend فیلدی را ارسال می‌کند که دیگر وجود ندارد.
  • Backend ساختار Response را تغییر می‌دهد.
  • یک مقدار اختیاری به فیلد اجباری تبدیل می‌شود.
  • نام Endpoint یا پارامتر تغییر می‌کند.
  • Client همچنان Typeهای قدیمی را استفاده می‌کند.
  • خطا فقط هنگام اجرای برنامه مشخص می‌شود.

در REST API می‌توان این مشکل را با OpenAPI و تولید SDK کاهش داد. در GraphQL نیز Schema و Code Generator می‌توانند Typeهای Client را تولید کنند. tRPC راه متفاوتی ارائه می‌دهد: وقتی Frontend و Backend هر دو با TypeScript نوشته شده‌اند، Type روتر Backend مستقیماً در Client استفاده می‌شود.

در این مقاله tRPC را از صفر راه‌اندازی می‌کنیم، Query و Mutation می‌سازیم، ورودی‌ها را با Zod اعتبارسنجی می‌کنیم و یک برنامه React را به Backend متصل می‌کنیم. در پایان نیز یک Procedure واقعی برای اتصال امن به API هوش مصنوعی درواره خواهیم ساخت.

tRPC چیست؟

tRPC یک فریم‌ورک برای ساخت APIهای End-to-End Type-Safe در پروژه‌های TypeScript است. با tRPC می‌توان Procedureهای Backend را تعریف کرد و سپس ورودی و خروجی همان Procedureها را بدون تعریف Type تکراری یا تولید فایل SDK در Client استفاده کرد.

نمونه Procedure در Backend:

const userRouter = router({
  byId: publicProcedure
    .input(
      z.object({
        id: z.string(),
      }),
    )
    .query(({ input }) => {
      return {
        id: input.id,
        name: "Sara",
        active: true,
      };
    }),
});

در Frontend:

const user = await trpc.user.byId.query({
  id: "usr_123",
});

TypeScript به‌صورت خودکار می‌داند:

  • Procedure با نام user.byId وجود دارد.
  • ورودی باید یک Object شامل id از نوع String باشد.
  • خروجی دارای id، name و active است.
  • active از نوع Boolean است.
  • ارسال فیلد اشتباه یا استفاده نادرست از خروجی باعث خطای TypeScript می‌شود.

tRPC برای ایجاد این Type Safety به تولید فایل جداگانه نیاز ندارد. Type مربوط به Router از Backend به‌صورت Type-Only در Client وارد می‌شود.

براساس راهنمای رسمی tRPC، Client با دریافت Type مربوط به AppRouter می‌تواند Procedureها، ورودی‌ها و خروجی‌ها را استنباط کند و در ویرایشگر Autocomplete ارائه دهد.

عبارت tRPC مخفف چیست؟

نام tRPC از ترکیب TypeScript و RPC گرفته شده است. RPC مخفف Remote Procedure Call است؛ یعنی Client یک تابع یا Procedure را که روی Server قرار دارد، از راه دور فراخوانی می‌کند.

در tRPC، فراخوانی Client ممکن است چنین باشد:

await trpc.article.create.mutate({
  title: "آموزش tRPC",
  content: "متن مقاله",
});

این کد از نظر تجربه توسعه شبیه فراخوانی یک تابع TypeScript است، اما در عمل Client یک درخواست HTTP به Server ارسال می‌کند.

tRPC چگونه کار می‌کند؟

گردش ساده یک درخواست tRPC به این شکل است:

  1. Backend یک Router تعریف می‌کند.
  2. Router شامل Query، Mutation یا Subscription است.
  3. هر Procedure می‌تواند Input Validator داشته باشد.
  4. Type کامل Router با typeof appRouter استخراج می‌شود.
  5. Client این Type را با import type دریافت می‌کند.
  6. tRPC Client درخواست HTTP را می‌سازد.
  7. Adapter سمت Server درخواست را به Procedure مناسب هدایت می‌کند.
  8. نتیجه به Client برگردانده می‌شود.
  9. TypeScript نوع نتیجه را بدون Type دستی تشخیص می‌دهد.

نکته مهم این است که کد Backend به Frontend منتقل نمی‌شود. فقط Type مربوط به Router در زمان Compile استفاده و سپس حذف می‌شود.

مزایای tRPC

Type Safety سراسری

ورودی و خروجی API از Backend تا Frontend Type-Safe است.

const result =
  await trpc.chat.create.mutate({
    prompt: "سلام",
    modelId: "YOUR_MODEL_ID",
  });

اگر modelId حذف یا به نوع اشتباه ارسال شود، TypeScript قبل از اجرا هشدار می‌دهد.

حذف Typeهای تکراری

در پروژه‌های معمولی ممکن است Typeهای مشابه در چند محل تعریف شوند:

// Backend
type CreateUserInput = {
  name: string;
  email: string;
};

// Frontend
type CreateUserRequest = {
  name: string;
  email: string;
};

با tRPC، Type ورودی از Procedure استنباط می‌شود و نیازی به نگهداری دو تعریف مستقل نیست.

Autocomplete کامل

با نوشتن:

trpc.

ویرایشگر Routerها و Procedureهای موجود را پیشنهاد می‌دهد.

Refactoring ساده‌تر

اگر نام فیلد یا Procedure تغییر کند، TypeScript محل‌های ناسازگار در Client را نشان می‌دهد.

هماهنگی با Zod

tRPC از Validatorهای مختلف پشتیبانی می‌کند و Zod را به‌عنوان انتخاب پیشنهادی معرفی می‌کند. Schema ورودی هم داده Runtime را اعتبارسنجی می‌کند و هم Type ورودی را می‌سازد. جزئیات در مستندات Input & Output Validators آمده است.

هماهنگی با TanStack Query

در React می‌توان tRPC را با TanStack Query ترکیب کرد و از Cache، Loading State، Mutation، Retry و Invalidation استفاده کرد.

محدودیت‌های tRPC

tRPC برای تمام APIها بهترین انتخاب نیست.

وابستگی Client و Server به TypeScript

مزیت اصلی tRPC زمانی به دست می‌آید که Client و Server هر دو TypeScript باشند. اگر API باید توسط Python، PHP، Java، Go یا مشتریان خارجی استفاده شود، REST و OpenAPI یا GraphQL ممکن است قرارداد عمومی مناسب‌تری ارائه دهند.

اتصال نسبتاً نزدیک Frontend و Backend

Frontend مستقیماً به Type روتر Backend وابسته است. این ویژگی در Monorepo یا محصول Full-Stack بسیار مفید است، اما برای API عمومی و تیم‌های کاملاً مستقل ممکن است محدودکننده باشد.

نبود قرارداد مستقل عمومی به شکل پیش‌فرض

در REST می‌توان یک فایل OpenAPI منتشر کرد. در GraphQL نیز Schema قابل Introspection است. در tRPC قرارداد اصلی همان TypeScript Router است.

مناسب نبودن برای API عمومی چندزبانه

اگر توسعه‌دهندگان خارج سازمان باید با زبان‌های مختلف به API متصل شوند، بهتر است یک لایه REST، OpenAPI یا GraphQL عمومی نیز در نظر گرفته شود.

Type Safety فقط در Compile Time کافی نیست

حتی با tRPC باید ورودی Runtime اعتبارسنجی شود. به همین دلیل از Zod یا Validator مشابه استفاده می‌کنیم.

tRPC، REST و GraphQL چه تفاوتی دارند؟

معیارtRPCRESTGraphQL
مدل ارتباطProcedureEndpoint و ResourceQuery و Schema
Type Safety خودکاربسیار قوی در TypeScriptبا OpenAPI و Codegenبا Schema و Codegen
مناسب API عمومیمحدودتربسیار مناسبمناسب
پشتیبانی چند زبانمحدودتربسیار گستردهگسترده
نیاز به Code Generationمعمولاً نداردبرای SDK اغلب داردبرای Client Type اغلب دارد
انتخاب فیلد توسط Clientمحدود به Procedureمحدود به Endpointدارد
بهترین کاربردبرنامه Full-Stack TypeScriptAPI عمومی و استانداردداده‌های گرافی و Clientهای متنوع
پیچیدگی شروعکم تا متوسطکممتوسط

انتخاب ابزار باید براساس معماری محصول انجام شود:

  • اگر Frontend و Backend در یک پروژه TypeScript هستند، tRPC بسیار جذاب است.
  • اگر API برای مشتریان عمومی و چند زبان ارائه می‌شود، REST و OpenAPI انتخاب رایج‌تری هستند.
  • اگر Client باید ساختار داده مورد نیازش را انتخاب کند، GraphQL می‌تواند مناسب باشد.
  • در بعضی سیستم‌ها می‌توان tRPC را برای داشبورد داخلی و REST را برای API عمومی استفاده کرد.

مفاهیم اصلی tRPC

Router

Router مجموعه‌ای از Procedureها یا Routerهای کوچک‌تر است.

const appRouter = router({
  user: userRouter,
  article: articleRouter,
  chat: chatRouter,
});

Procedure

Procedure تابعی است که از Client قابل فراخوانی است.

طبق مستندات رسمی Procedureها، Procedure می‌تواند یکی از این انواع باشد:

  • Query برای دریافت داده
  • Mutation برای ایجاد، ویرایش یا حذف داده
  • Subscription برای جریان داده بلادرنگ

Context

Context داده‌ای است که در هر Procedure قابل دسترسی خواهد بود، مانند:

  • اتصال دیتابیس
  • کاربر فعلی
  • Session
  • Request ID
  • Logger
  • تنظیمات برنامه

Middleware

Middleware پیش از اجرای Procedure اجرا می‌شود و برای موارد مشترکی مانند Logging، بررسی Session یا اندازه‌گیری زمان درخواست کاربرد دارد.

Input Validator

ورودی درخواست را در Runtime بررسی می‌کند:

.input(
  z.object({
    id: z.string().uuid(),
  }),
)

Output Validator

در صورت نیاز می‌توان خروجی Procedure را نیز اعتبارسنجی کرد:

.output(
  z.object({
    id: z.string(),
    name: z.string(),
  }),
)

نسخه مورد استفاده در این آموزش

نمونه‌های این مقاله براساس tRPC 11 نوشته شده‌اند. tRPC v11 از TanStack React Query جدید پشتیبانی می‌کند و برای استفاده کامل از آن باید نسخه‌های هماهنگ کتابخانه‌ها را نصب کنید.

در پروژه‌های قدیمی، پیش از ارتقا راهنمای مهاجرت از tRPC 10 به tRPC 11 را بررسی کنید.

معماری پروژه عملی

ساختار پیشنهادی:

trpc-ai-app/
  apps/
    server/
      src/
        context.ts
        server.ts
        trpc.ts
        routers/
          chat.router.ts
          model.router.ts
          index.ts
    web/
      src/
        App.tsx
        main.tsx
        trpc.tsx
        components/
          ChatForm.tsx
          ModelList.tsx
  packages/
    shared/
  package.json

برای ساده نگه داشتن آموزش، Server و Client را جدا می‌سازیم؛ اما Type مربوط به AppRouter باید برای Client قابل دسترسی باشد. در پروژه واقعی بهتر است از Monorepo مانند npm Workspaces، pnpm Workspaces یا Turborepo استفاده شود.

ساخت Backend با Node.js و Express

پوشه Server را بسازید:

mkdir trpc-server
cd trpc-server
npm init -y

وابستگی‌ها:

npm install @trpc/server zod express cors dotenv
npm install --save-dev typescript tsx @types/node @types/express @types/cors

در package.json:

{
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js",
    "typecheck": "tsc --noEmit"
  }
}

فایل tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": [
    "src"
  ]
}

راه‌اندازی tRPC

فایل src/trpc.ts:

import { initTRPC } from "@trpc/server";

type Context = {
  requestId: string;
  user: {
    id: string;
    name: string;
  } | null;
};

const t = initTRPC
  .context<Context>()
  .create({
    isDev:
      process.env.NODE_ENV !== "production",
  });

export const router = t.router;
export const publicProcedure = t.procedure;
export const middleware = t.middleware;

در این فایل:

  • Shape مربوط به Context تعریف شده است.
  • tRPC با Context مورد نظر راه‌اندازی شده است.
  • Router و Procedure پایه صادر شده‌اند.
  • نمایش جزئیات Development براساس محیط کنترل می‌شود.

ساخت Context

فایل src/context.ts:

import type {
  CreateExpressContextOptions,
} from "@trpc/server/adapters/express";

export async function createContext({
  req,
}: CreateExpressContextOptions) {
  const requestId =
    req.header("x-request-id") ??
    crypto.randomUUID();

  const userId =
    req.header("x-demo-user-id");

  const user = userId
    ? {
        id: userId,
        name: "Demo User",
      }
    : null;

  return {
    requestId,
    user,
  };
}

export type Context = Awaited<
  ReturnType<typeof createContext>
>;

استفاده از x-demo-user-id در این مثال فقط برای آموزش Context است و احراز هویت واقعی محسوب نمی‌شود. در پروژه واقعی، Context باید اطلاعات کاربر را از Session یا سازوکار احراز هویت معتبر برنامه دریافت کند.

ساخت اولین Router

فایل src/routers/model.router.ts:

import { z } from "zod";
import {
  publicProcedure,
  router,
} from "../trpc.js";

const models = [
  {
    id: "model-text-pro",
    name: "مدل متنی حرفه‌ای",
    category: "text",
  },
  {
    id: "model-code-pro",
    name: "مدل برنامه‌نویسی",
    category: "coding",
  },
  {
    id: "model-reasoning-pro",
    name: "مدل استدلالی",
    category: "reasoning",
  },
];

export const modelRouter = router({
  list: publicProcedure
    .input(
      z
        .object({
          category: z
            .enum([
              "text",
              "coding",
              "reasoning",
            ])
            .optional(),
        })
        .optional(),
    )
    .query(({ input }) => {
      if (!input?.category) {
        return models;
      }

      return models.filter(
        (model) =>
          model.category === input.category,
      );
    }),

  byId: publicProcedure
    .input(
      z.object({
        id: z.string().min(1),
      }),
    )
    .query(({ input }) => {
      return (
        models.find(
          (model) => model.id === input.id,
        ) ?? null
      );
    }),
});

در این Router دو Query داریم:

  • model.list
  • model.byId

Type ورودی و خروجی هر دو توسط TypeScript استنباط می‌شوند.

Query در tRPC چیست؟

Query برای دریافت داده‌ای استفاده می‌شود که معمولاً Side Effect ندارد:

const articleRouter = router({
  list: publicProcedure.query(() => {
    return [
      {
        id: "1",
        title: "آموزش TypeScript",
      },
    ];
  }),
});

فراخوانی Client:

const articles =
  await trpc.article.list.query();

Mutation در tRPC چیست؟

Mutation برای عملیات ایجاد، ویرایش، حذف یا اجرای یک فرایند استفاده می‌شود:

const articleRouter = router({
  create: publicProcedure
    .input(
      z.object({
        title: z.string().min(5),
        content: z.string().min(20),
      }),
    )
    .mutation(({ input }) => {
      return {
        id: crypto.randomUUID(),
        ...input,
        createdAt: new Date(),
      };
    }),
});

فراخوانی Client:

const article =
  await trpc.article.create.mutate({
    title: "آموزش tRPC",
    content:
      "متن کامل مقاله آموزشی درباره tRPC",
  });

اعتبارسنجی ورودی با Zod

Schema نمونه ساخت مکالمه:

const CreateChatInputSchema = z.object({
  modelId: z.string().min(1),
  prompt: z
    .string()
    .trim()
    .min(1)
    .max(10_000),
  temperature: z
    .number()
    .min(0)
    .max(2)
    .optional(),
});

در Procedure:

create: publicProcedure
  .input(CreateChatInputSchema)
  .mutation(({ input }) => {
    return {
      modelId: input.modelId,
      prompt: input.prompt,
      temperature:
        input.temperature ?? 0.7,
    };
  });

اگر Client این ورودی را ارسال کند:

{
  modelId: "",
  prompt: "",
  temperature: 5
}

Zod درخواست را پیش از اجرای Resolver رد می‌کند.

TypeScript خطاهای قابل شناسایی در Compile Time را پیدا می‌کند و Zod داده واقعی Runtime را اعتبارسنجی می‌کند.

ساخت Protected Procedure

فایل src/trpc.ts را گسترش می‌دهیم:

import {
  initTRPC,
  TRPCError,
} from "@trpc/server";

type Context = {
  requestId: string;
  user: {
    id: string;
    name: string;
  } | null;
};

const t = initTRPC
  .context<Context>()
  .create();

export const router = t.router;
export const publicProcedure = t.procedure;

export const protectedProcedure =
  t.procedure.use(({ ctx, next }) => {
    if (!ctx.user) {
      throw new TRPCError({
        code: "UNAUTHORIZED",
        message:
          "برای اجرای این عملیات باید وارد حساب شوید.",
      });
    }

    return next({
      ctx: {
        ...ctx,
        user: ctx.user,
      },
    });
  });

پس از Middleware، TypeScript می‌داند که ctx.user دیگر null نیست:

const profileRouter = router({
  me: protectedProcedure.query(
    ({ ctx }) => {
      return ctx.user;
    },
  ),
});

راهنمای رسمی این الگو در مستندات Authorization در tRPC موجود است.

ساخت Middleware برای Logging

const loggedProcedure =
  publicProcedure.use(
    async ({ path, ctx, next }) => {
      const startedAt = performance.now();

      const result = await next();

      const durationMs =
        performance.now() - startedAt;

      console.log({
        requestId: ctx.requestId,
        path,
        durationMs:
          Math.round(durationMs),
        ok: result.ok,
      });

      return result;
    },
  );

استفاده:

const healthRouter = router({
  check: loggedProcedure.query(() => {
    return {
      status: "ok",
      timestamp: new Date(),
    };
  }),
});

در Production بهتر است Log ساختاریافته باشد و اطلاعات حساس، متن کامل پرامپت یا Secretها بدون سیاست مشخص ثبت نشوند.

ترکیب Routerها

فایل src/routers/index.ts:

import { router } from "../trpc.js";
import { modelRouter } from "./model.router.js";

export const appRouter = router({
  model: modelRouter,
});

export type AppRouter =
  typeof appRouter;

AppRouter مهم‌ترین Type پروژه tRPC است. Client با استفاده از همین Type ساختار کامل API را می‌شناسد.

اتصال tRPC به Express

فایل src/server.ts:

import "dotenv/config";
import express from "express";
import cors from "cors";
import {
  createExpressMiddleware,
} from "@trpc/server/adapters/express";
import { createContext } from "./context.js";
import { appRouter } from "./routers/index.js";

const app = express();
const port = Number(
  process.env.PORT ?? 3000,
);

app.use(
  cors({
    origin: "http://localhost:5173",
    credentials: true,
  }),
);

app.get("/health", (_req, res) => {
  res.json({
    status: "ok",
  });
});

app.use(
  "/trpc",
  createExpressMiddleware({
    router: appRouter,
    createContext,
    onError({
      error,
      path,
      ctx,
    }) {
      console.error({
        requestId: ctx?.requestId,
        path,
        code: error.code,
        message: error.message,
      });
    },
  }),
);

app.listen(port, () => {
  console.log(
    `Server is running on http://localhost:${port}`,
  );
});

اجرا:

npm run dev

Health Check:

curl http://localhost:3000/health

ساخت Client ساده tRPC

در یک پروژه TypeScript:

npm install @trpc/client

فایل src/client.ts:

import {
  createTRPCClient,
  httpBatchLink,
} from "@trpc/client";
import type {
  AppRouter,
} from "../server/routers/index.js";

const trpc =
  createTRPCClient<AppRouter>({
    links: [
      httpBatchLink({
        url:
          "http://localhost:3000/trpc",
      }),
    ],
  });

const models =
  await trpc.model.list.query();

console.log(models);

باید AppRouter را با import type وارد کنید:

import type { AppRouter } from "...";

این کار باعث می‌شود کد اجرایی Server وارد Bundle مربوط به Client نشود.

httpBatchLink چند عملیات tRPC را در صورت امکان داخل یک درخواست HTTP گروه‌بندی می‌کند.

const [models, profile] =
  await Promise.all([
    trpc.model.list.query(),
    trpc.profile.me.query(),
  ]);

طبق مستندات HTTP Batch Link، این Link می‌تواند چند عملیات هم‌زمان را در یک درخواست HTTP ارسال کند.

برای کنترل تعداد عملیات هر Batch:

httpBatchLink({
  url: "http://localhost:3000/trpc",
  maxItems: 10,
});

Batching همیشه به معنی یک Query دیتابیس نیست. هر Procedure ممکن است Query یا پردازش مستقل خود را اجرا کند.

ساخت برنامه React

با Vite پروژه React را بسازید:

npm create vite@latest trpc-web -- --template react-ts
cd trpc-web
npm install

وابستگی‌های tRPC و TanStack Query:

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

در Monorepo معمولاً Type مربوط به AppRouter از یک Workspace داخلی دریافت می‌شود.

ساخت tRPC Provider در React

فایل src/trpc.tsx:

import {
  QueryClient,
  QueryClientProvider,
} from "@tanstack/react-query";
import {
  createTRPCClient,
  httpBatchLink,
} from "@trpc/client";
import {
  createTRPCContext,
} from "@trpc/tanstack-react-query";
import {
  useState,
  type ReactNode,
} from "react";
import type {
  AppRouter,
} from "../../server/src/routers/index";

export const {
  TRPCProvider,
  useTRPC,
  useTRPCClient,
} = createTRPCContext<AppRouter>();

type Props = {
  children: ReactNode;
};

export function AppProviders({
  children,
}: Props) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 30_000,
            retry: 1,
          },
        },
      }),
  );

  const [trpcClient] = useState(
    () =>
      createTRPCClient<AppRouter>({
        links: [
          httpBatchLink({
            url:
              "http://localhost:3000/trpc",
            headers() {
              return {
                "x-demo-user-id":
                  "usr_demo",
              };
            },
          }),
        ],
      }),
  );

  return (
    <QueryClientProvider
      client={queryClient}
    >
      <TRPCProvider
        trpcClient={trpcClient}
        queryClient={queryClient}
      >
        {children}
      </TRPCProvider>
    </QueryClientProvider>
  );
}

در src/main.tsx:

import { StrictMode } from "react";
import {
  createRoot,
} from "react-dom/client";
import App from "./App";
import {
  AppProviders,
} from "./trpc";

createRoot(
  document.getElementById("root")!,
).render(
  <StrictMode>
    <AppProviders>
      <App />
    </AppProviders>
  </StrictMode>,
);

ساختار به‌روز اتصال tRPC 11 به React در مستندات TanStack React Query Setup توضیح داده شده است.

اجرای Query در React

فایل src/components/ModelList.tsx:

import {
  useQuery,
} from "@tanstack/react-query";
import { useTRPC } from "../trpc";

export function ModelList() {
  const trpc = useTRPC();

  const modelsQuery = useQuery(
    trpc.model.list.queryOptions(),
  );

  if (modelsQuery.isPending) {
    return <p>در حال دریافت مدل‌ها...</p>;
  }

  if (modelsQuery.isError) {
    return (
      <p>
        دریافت مدل‌ها انجام نشد.
      </p>
    );
  }

  return (
    <ul>
      {modelsQuery.data.map(
        (model) => (
          <li key={model.id}>
            <strong>
              {model.name}
            </strong>
            <span>
              {" "}
              {model.category}
            </span>
          </li>
        ),
      )}
    </ul>
  );
}

Type مربوط به modelsQuery.data از خروجی Procedure استنباط می‌شود. نیازی به تعریف دستی Model در Frontend نداریم.

اجرای Query دارای ورودی

const modelQuery = useQuery(
  trpc.model.byId.queryOptions({
    id: selectedModelId,
  }),
);

TypeScript اجازه نمی‌دهد این ورودی ارسال شود:

trpc.model.byId.queryOptions({
  modelIdentifier: 123,
});

زیرا Procedure یک فیلد id از نوع String می‌خواهد.

ساخت Procedure اتصال به هوش مصنوعی

فایل src/routers/chat.router.ts:

import { z } from "zod";
import {
  protectedProcedure,
  router,
} from "../trpc.js";

const ChatCompletionSchema = z.object({
  id: z.string().optional(),
  choices: z
    .array(
      z.object({
        message: z.object({
          role: z.string(),
          content: z
            .string()
            .nullable(),
        }),
      }),
    )
    .min(1),
  usage: z
    .object({
      prompt_tokens:
        z.number().optional(),
      completion_tokens:
        z.number().optional(),
      total_tokens:
        z.number().optional(),
    })
    .optional(),
});

export const chatRouter = router({
  create: protectedProcedure
    .input(
      z.object({
        modelId: z.string().min(1),
        prompt: z
          .string()
          .trim()
          .min(1)
          .max(10_000),
        temperature: z
          .number()
          .min(0)
          .max(2)
          .default(0.7),
      }),
    )
    .output(
      z.object({
        content: z.string(),
        modelId: z.string(),
        usage: z
          .object({
            inputTokens:
              z.number().optional(),
            outputTokens:
              z.number().optional(),
            totalTokens:
              z.number().optional(),
          })
          .optional(),
      }),
    )
    .mutation(
      async ({ input, ctx }) => {
        const apiKey =
          process.env
            .DARVAREH_API_KEY;

        if (!apiKey) {
          throw new Error(
            "AI service is not configured",
          );
        }

        const response = await fetch(
          "https://api.darvareh.ir/v1/chat/completions",
          {
            method: "POST",
            headers: {
              Authorization:
                `Bearer ${apiKey}`,
              "Content-Type":
                "application/json",
              "x-request-id":
                ctx.requestId,
            },
            body: JSON.stringify({
              model: input.modelId,
              temperature:
                input.temperature,
              messages: [
                {
                  role: "user",
                  content: input.prompt,
                },
              ],
            }),
            signal:
              AbortSignal.timeout(
                60_000,
              ),
          },
        );

        if (!response.ok) {
          throw new Error(
            `AI request failed: ${response.status}`,
          );
        }

        const rawData: unknown =
          await response.json();

        const result =
          ChatCompletionSchema.safeParse(
            rawData,
          );

        if (!result.success) {
          throw new Error(
            "AI response structure is invalid",
          );
        }

        const content =
          result.data.choices[0]
            .message.content;

        if (!content) {
          throw new Error(
            "AI response is empty",
          );
        }

        return {
          content,
          modelId: input.modelId,
          usage: result.data.usage
            ? {
                inputTokens:
                  result.data.usage
                    .prompt_tokens,
                outputTokens:
                  result.data.usage
                    .completion_tokens,
                totalTokens:
                  result.data.usage
                    .total_tokens,
              }
            : undefined,
        };
      },
    ),
});

Router جدید را به AppRouter اضافه کنید:

import { router } from "../trpc.js";
import { chatRouter } from "./chat.router.js";
import { modelRouter } from "./model.router.js";

export const appRouter = router({
  chat: chatRouter,
  model: modelRouter,
});

export type AppRouter =
  typeof appRouter;

متغیرهای محیطی:

PORT=3000
DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID

کلید API باید فقط در Backend نگهداری شود. tRPC باعث Type-Safe شدن ارتباط Frontend و Backend می‌شود، اما قرار دادن Secret در Frontend را امن نمی‌کند.

برای دریافت API Key می‌توانید در درواره ثبت‌نام کنید. شناسه مدل‌ها، قابلیت‌ها و قیمت‌های به‌روز نیز در صفحه مدل‌های درواره قرار دارند.

اجرای Mutation هوش مصنوعی در React

فایل src/components/ChatForm.tsx:

import {
  useMutation,
} from "@tanstack/react-query";
import {
  useState,
  type FormEvent,
} from "react";
import { useTRPC } from "../trpc";

export function ChatForm() {
  const trpc = useTRPC();
  const [prompt, setPrompt] =
    useState("");
  const [modelId, setModelId] =
    useState("YOUR_MODEL_ID");

  const chatMutation =
    useMutation(
      trpc.chat.create.mutationOptions(),
    );

  function handleSubmit(
    event: FormEvent,
  ) {
    event.preventDefault();

    if (!prompt.trim()) {
      return;
    }

    chatMutation.mutate({
      prompt,
      modelId,
      temperature: 0.7,
    });
  }

  return (
    <section>
      <form onSubmit={handleSubmit}>
        <label>
          شناسه مدل
          <input
            value={modelId}
            onChange={(event) =>
              setModelId(
                event.target.value,
              )
            }
          />
        </label>

        <label>
          پیام
          <textarea
            value={prompt}
            onChange={(event) =>
              setPrompt(
                event.target.value,
              )
            }
          />
        </label>

        <button
          type="submit"
          disabled={
            chatMutation.isPending
          }
        >
          {chatMutation.isPending
            ? "در حال پردازش..."
            : "ارسال"}
        </button>
      </form>

      {chatMutation.isError && (
        <p>
          پردازش درخواست انجام نشد.
        </p>
      )}

      {chatMutation.data && (
        <article>
          <h2>پاسخ</h2>
          <p>
            {
              chatMutation.data
                .content
            }
          </p>

          {chatMutation.data
            .usage && (
            <small>
              مجموع توکن‌ها:{" "}
              {chatMutation.data
                .usage
                .totalTokens ??
                "نامشخص"}
            </small>
          )}
        </article>
      )}
    </section>
  );
}

TypeScript ساختار ورودی Mutation و خروجی آن را از chat.create تشخیص می‌دهد.

اگر Backend نام content را به answer تغییر دهد، محل استفاده قدیمی در Frontend خطای TypeScript خواهد گرفت.

Invalidate کردن Query پس از Mutation

فرض کنید پس از ساخت Conversation باید فهرست مکالمات دوباره دریافت شود:

import {
  useMutation,
  useQueryClient,
} from "@tanstack/react-query";
import { useTRPC } from "../trpc";

function CreateConversationButton() {
  const trpc = useTRPC();
  const queryClient =
    useQueryClient();

  const mutation = useMutation(
    trpc.conversation.create
      .mutationOptions({
        onSuccess: async () => {
          await queryClient.invalidateQueries(
            trpc.conversation.list
              .queryFilter(),
          );
        },
      }),
  );

  return (
    <button
      onClick={() =>
        mutation.mutate({
          title: "مکالمه جدید",
          modelId:
            "YOUR_MODEL_ID",
        })
      }
    >
      ساخت مکالمه
    </button>
  );
}

این ترکیب مزایای TanStack Query مانند Cache Invalidation و State Management درخواست را با Typeهای tRPC همراه می‌کند.

مدیریت خطا در tRPC

برای خطاهای شناخته‌شده از TRPCError استفاده کنید:

import {
  TRPCError,
} from "@trpc/server";

throw new TRPCError({
  code: "NOT_FOUND",
  message:
    "مکالمه مورد نظر پیدا نشد.",
});

کدهای رایج:

  • BAD_REQUEST
  • UNAUTHORIZED
  • FORBIDDEN
  • NOT_FOUND
  • CONFLICT
  • TOO_MANY_REQUESTS
  • INTERNAL_SERVER_ERROR
  • TIMEOUT

نمونه:

const conversation =
  await findConversation(
    input.id,
  );

if (!conversation) {
  throw new TRPCError({
    code: "NOT_FOUND",
    message:
      "مکالمه پیدا نشد.",
  });
}

tRPC خطا را به ساختار قابل‌استفاده در Client تبدیل می‌کند. براساس مستندات Error Handling، Response خطا شامل اطلاعاتی مانند Code، HTTP Status و مسیر Procedure است.

در محیط Production نباید Stack Trace و جزئیات داخلی به Client نمایش داده شوند. tRPC به‌صورت پیش‌فرض Stack را فقط در حالت Development قرار می‌دهد، اما تنظیم محیط استقرار باید صحیح باشد.

سفارشی‌سازی Error Format

const t = initTRPC
  .context<Context>()
  .create({
    errorFormatter({
      shape,
      error,
    }) {
      return {
        ...shape,
        data: {
          ...shape.data,
          validation:
            error.cause instanceof
            z.ZodError
              ? z.flattenError(
                  error.cause,
                ).fieldErrors
              : null,
        },
      };
    },
  });

این روش می‌تواند خطاهای Validation را برای فرم React قابل‌استفاده‌تر کند.

پیام‌های فنی و اطلاعات حساس را بدون بررسی به Response عمومی اضافه نکنید.

اتصال tRPC به Prisma

می‌توان Prisma Client را در Context قرار داد:

import { prisma } from "./lib/prisma.js";

export async function createContext({
  req,
}: CreateExpressContextOptions) {
  const user =
    await getCurrentUser(req);

  return {
    prisma,
    user,
    requestId:
      req.header("x-request-id") ??
      crypto.randomUUID(),
  };
}

Procedure:

const conversationRouter = router({
  list: protectedProcedure.query(
    async ({ ctx }) => {
      return ctx.prisma
        .conversation
        .findMany({
          where: {
            userId:
              ctx.user.id,
          },
          select: {
            id: true,
            title: true,
            modelId: true,
            updatedAt: true,
          },
          orderBy: {
            updatedAt: "desc",
          },
          take: 50,
        });
    },
  ),
});

حتی اگر Client شناسه کاربر دیگری ارسال کند، Query باید مالکیت داده را با اطلاعات کاربر موجود در Context محدود کند.

تفکیک Routerها در پروژه بزرگ

Router اصلی را به فایل‌های کوچک تقسیم کنید:

src/
  routers/
    user.router.ts
    conversation.router.ts
    message.router.ts
    model.router.ts
    chat.router.ts
    index.ts

Router اصلی:

export const appRouter = router({
  user: userRouter,
  conversation:
    conversationRouter,
  message: messageRouter,
  model: modelRouter,
  chat: chatRouter,
});

این ساختار خوانایی و تست‌پذیری پروژه را بهتر می‌کند.

استخراج Type ورودی و خروجی

گاهی برای Service، Component یا تست به Type یک Procedure نیاز دارید.

در سطح کامل Router:

import type {
  inferRouterInputs,
  inferRouterOutputs,
} from "@trpc/server";
import type {
  AppRouter,
} from "./routers/index.js";

type RouterInputs =
  inferRouterInputs<AppRouter>;

type RouterOutputs =
  inferRouterOutputs<AppRouter>;

type CreateChatInput =
  RouterInputs["chat"]["create"];

type CreateChatOutput =
  RouterOutputs["chat"]["create"];

اکنون Typeها بدون تعریف مجدد از Router استخراج می‌شوند.

در ادغام TanStack Query نیز ابزارهای inferInput و inferOutput وجود دارند. نمونه‌های آن در راهنمای استفاده از TanStack React Query ارائه شده‌اند.

تست Router با createCaller

می‌توان Router را بدون اجرای HTTP Server تست کرد:

import {
  describe,
  expect,
  it,
} from "vitest";
import { appRouter } from "./routers/index.js";

describe("model router", () => {
  it("returns model list", async () => {
    const caller =
      appRouter.createCaller({
        requestId: "test-request",
        user: {
          id: "usr_test",
          name: "Test User",
        },
      });

    const models =
      await caller.model.list();

    expect(
      models.length,
    ).toBeGreaterThan(0);
  });
});

تست ورودی نامعتبر:

it(
  "rejects empty model id",
  async () => {
    const caller =
      appRouter.createCaller({
        requestId:
          "test-request",
        user: null,
      });

    await expect(
      caller.model.byId({
        id: "",
      }),
    ).rejects.toThrow();
  },
);

createCaller Procedureها و Middlewareها را مستقیماً اجرا می‌کند. جزئیات در مستندات Server-Side Calls موجود است.

آیا از createCaller داخل Procedure استفاده کنیم؟

معمولاً بهتر است Business Logic را در Service جداگانه قرار دهید و هم Procedureها و هم کد Server آن Service را فراخوانی کنند.

روش نامناسب:

const result =
  await caller.chat.create(input);

داخل یک Procedure دیگر ممکن است Middlewareها و Validationها را دوباره اجرا کند.

روش بهتر:

async function createChat(
  input: CreateChatInput,
  dependencies: Dependencies,
) {
  // Business logic
}

سپس Procedure:

create: protectedProcedure
  .input(CreateChatInputSchema)
  .mutation(({ input, ctx }) => {
    return createChat(
      input,
      {
        user: ctx.user,
        prisma: ctx.prisma,
      },
    );
  });

Data Transformer

JSON به‌صورت مستقیم بعضی Typeها مانند Date، Map، Set و BigInt را به شکل اصلی منتقل نمی‌کند.

برای Typeهای پیچیده می‌توان از Transformer سازگار استفاده کرد. اگر از Transformer استفاده می‌کنید، باید آن را در Server و Client یکسان تنظیم کنید.

قبل از افزودن Transformer بررسی کنید آیا API واقعاً نیاز دارد Typeهای پیچیده را مستقیماً منتقل کند یا بهتر است آن‌ها را به رشته یا Object ساده تبدیل کند.

Subscription در tRPC

Subscription برای داده‌های بلادرنگ کاربرد دارد:

  • اعلان‌ها
  • وضعیت Job
  • پیشرفت تولید ویدئو
  • تغییر وضعیت Agent
  • رویدادهای زنده
  • Streaming داده

در tRPC 11 می‌توان از Async Generator و روش‌های انتقال مانند SSE یا WebSocket استفاده کرد.

نمونه مفهومی:

const jobRouter = router({
  progress:
    protectedProcedure
      .input(
        z.object({
          jobId: z.string(),
        }),
      )
      .subscription(
        async function* ({
          input,
          signal,
        }) {
          while (
            !signal?.aborted
          ) {
            const progress =
              await getJobProgress(
                input.jobId,
              );

            yield progress;

            if (
              progress.status ===
                "completed" ||
              progress.status ===
                "failed"
            ) {
              return;
            }

            await new Promise(
              (resolve) =>
                setTimeout(
                  resolve,
                  1000,
                ),
            );
          }
        },
      ),
});

برای بسیاری از کاربردها Polling ساده یا SSE کافی است. قبل از افزودن WebSocket، نیاز واقعی و هزینه نگهداری آن را بررسی کنید.

tRPC و Streaming پاسخ هوش مصنوعی

Streaming Tokenهای مدل هوش مصنوعی با Mutation معمولی که یک Response نهایی برمی‌گرداند متفاوت است.

گزینه‌های ممکن:

  • Streaming Query یا Mutation در tRPC 11
  • Subscription مبتنی بر SSE
  • Endpoint اختصاصی Streaming
  • WebSocket
  • ذخیره Job و Polling وضعیت

اگر API عمومی درواره پاسخ SSE ارائه می‌دهد، می‌توانید Streaming را در Backend دریافت و از طریق یک مسیر Streaming متناسب به Client منتقل کنید.

برای اولین نسخه محصول، پاسخ غیرجریانی ساده‌تر است. پس از تثبیت مدیریت خطا، لغو درخواست و ثبت مصرف می‌توان Streaming را اضافه کرد.

Batching و عملکرد

httpBatchLink می‌تواند چند عملیات هم‌زمان را در یک HTTP Request قرار دهد:

await Promise.all([
  trpc.user.me.query(),
  trpc.model.list.query(),
  trpc.conversation.list.query(),
]);

مزایا:

  • کاهش تعداد Requestهای HTTP
  • کاهش بخشی از سربار شبکه
  • ساده‌تر شدن بارگذاری هم‌زمان صفحه

اما باید محدودیت‌هایی نیز در نظر گرفته شوند:

  • Batch بسیار بزرگ می‌تواند Response سنگین ایجاد کند.
  • هر Procedure همچنان عملیات خود را اجرا می‌کند.
  • یک درخواست بزرگ ممکن است زمان بیشتری درگیر بماند.
  • Timeout و محدودیت اندازه Request باید کنترل شوند.

نمونه محدودسازی:

httpBatchLink({
  url: "/trpc",
  maxItems: 10,
  maxURLLength: 2_000,
});

جلوگیری از Queryهای سنگین

tRPC به‌صورت خودکار Query دیتابیس را بهینه نمی‌کند. برای عملکرد مناسب:

  • خروجی لیست‌ها را صفحه‌بندی کنید.
  • تعداد آیتم‌های ورودی را محدود کنید.
  • Relationهای بزرگ را بدون محدودیت نخوانید.
  • فقط فیلدهای موردنیاز را انتخاب کنید.
  • Queryهای دیتابیس را Index کنید.
  • Procedureهای کند را اندازه‌گیری کنید.
  • Timeout مناسب برای API خارجی قرار دهید.
  • تعداد عملیات Batch را محدود کنید.
  • پاسخ‌های قابل Cache را شناسایی کنید.

طراحی Pagination در tRPC

Schema:

const PaginationSchema = z.object({
  cursor: z
    .string()
    .nullish(),
  limit: z
    .number()
    .int()
    .min(1)
    .max(100)
    .default(20),
});

Procedure:

list: protectedProcedure
  .input(PaginationSchema)
  .query(async ({ input, ctx }) => {
    const items =
      await ctx.prisma
        .conversation
        .findMany({
          where: {
            userId:
              ctx.user.id,
          },
          take:
            input.limit + 1,
          cursor: input.cursor
            ? {
                id: input.cursor,
              }
            : undefined,
          skip: input.cursor
            ? 1
            : 0,
          orderBy: {
            id: "desc",
          },
        });

    let nextCursor:
      | string
      | undefined;

    if (
      items.length >
      input.limit
    ) {
      const nextItem =
        items.pop();

      nextCursor =
        nextItem?.id;
    }

    return {
      items,
      nextCursor,
    };
  });

در React می‌توان این Procedure را با Infinite Query در TanStack Query ترکیب کرد.

نسخه‌بندی API در tRPC

در یک Monorepo که Client و Server هم‌زمان منتشر می‌شوند، تغییرات Type توسط Build شناسایی می‌شوند. اما اگر نسخه‌های Client و Server جداگانه Deploy شوند، باید سازگاری را مدیریت کنید.

راهکارها:

  • تغییرات Breaking را تدریجی اعمال کنید.
  • ابتدا فیلد جدید را اضافه و اختیاری نگه دارید.
  • Clientها را به‌روزرسانی کنید.
  • سپس فیلد قدیمی را حذف کنید.
  • Router نسخه‌بندی‌شده بسازید:
const appRouter = router({
  v1: v1Router,
  v2: v2Router,
});
  • نسخه Client و Server را در Deployment هماهنگ کنید.
  • Contract Test داشته باشید.

Type Safety فقط زمانی مفید است که نسخه Client و Server با قرارداد سازگار اجرا شوند.

tRPC در Monorepo

یکی از بهترین محیط‌ها برای tRPC، Monorepo است:

apps/
  api/
  web/
packages/
  api-contract/
  database/
  shared/

Type مربوط به AppRouter می‌تواند از Package داخلی صادر شود:

export type {
  AppRouter,
} from "./routers/index";

Client فقط Type را وارد می‌کند:

import type {
  AppRouter,
} from "@project/api-contract";

مراقب باشید Package قرارداد، کد Server، Secretها، اتصال دیتابیس یا وابستگی‌های Node.js را وارد Bundle مرورگر نکند.

tRPC برای API عمومی مناسب است؟

اگر فقط یک Frontend TypeScript تحت کنترل خودتان دارید، tRPC انتخاب بسیار خوبی است.

اگر API باید توسط موارد زیر استفاده شود، بهتر است قرارداد عمومی دیگری نیز داشته باشید:

  • اپلیکیشن Python
  • مشتری PHP یا Laravel
  • اپلیکیشن Android با Kotlin
  • برنامه iOS با Swift
  • شرکای تجاری
  • کاربران خارجی
  • ابزارهایی مانند Postman
  • سرویس‌های Third-Party

در این حالت می‌توان:

  • API عمومی را REST و OpenAPI نگه داشت.
  • داشبورد داخلی را با tRPC ساخت.
  • Business Logic مشترک را میان هر دو Adapter استفاده کرد.

برای درواره نیز API عمومی مناسب توسعه‌دهندگان باید قرارداد استاندارد و مستقل داشته باشد؛ درحالی‌که tRPC می‌تواند برای ارتباط داخلی Dashboard TypeScript با Backend استفاده شود.

اشتباهات رایج در tRPC

وارد کردن AppRouter به‌صورت Runtime در Client

اشتباه:

import {
  AppRouter,
} from "../server";

درست:

import type {
  AppRouter,
} from "../server";

تصور اینکه TypeScript جای Validation را می‌گیرد

داده HTTP در Runtime دریافت می‌شود و باید با Zod یا Validator دیگر بررسی شود.

قرار دادن Secret در Frontend

tRPC کلید API را مخفی نمی‌کند. اتصال به سرویس هوش مصنوعی باید در Procedure سمت Server انجام شود.

استفاده از publicProcedure برای تمام عملیات

Procedureهای مربوط به داده کاربر باید Context و مجوز مناسب داشته باشند.

اعتماد به شناسه userId ارسال‌شده از Client

شناسه کاربر باید از Session معتبر در Context گرفته شود، نه از ورودی قابل تغییر Client.

قراردادن تمام Business Logic در Router

Router باید ورودی، Context و خروجی را مدیریت کند. منطق اصلی بهتر است در Serviceهای تست‌پذیر قرار گیرد.

ساخت Router بسیار بزرگ

Routerها را براساس Domain تقسیم کنید:

  • user
  • billing
  • conversation
  • message
  • model
  • chat

نادیده گرفتن Pagination

هیچ Procedure لیستی نباید بدون محدودیت، داده‌ای با رشد نامحدود برگرداند.

نمایش خطای داخلی

پیام دیتابیس، Stack Trace و جزئیات API خارجی نباید مستقیماً به کاربر نمایش داده شوند.

استفاده از tRPC برای تمام Clientهای خارجی

tRPC برای اکوسیستم TypeScript عالی است، اما برای مصرف‌کنندگان چندزبانه همیشه انتخاب مناسبی نیست.

نبود تست Runtime

موفق بودن Type Check جای تست Integration، تست دیتابیس و تست رفتار Procedure را نمی‌گیرد.

چک‌لیست tRPC برای Production

  • نسخه‌های @trpc/server و @trpc/client هماهنگ هستند.
  • TypeScript با strict فعال شده است.
  • AppRouter فقط با import type وارد Client می‌شود.
  • تمام Procedureهای دارای ورودی، Runtime Validator دارند.
  • Procedureهای خصوصی از Context معتبر استفاده می‌کنند.
  • مجوز دسترسی در Backend بررسی می‌شود.
  • شناسه کاربر از Session گرفته می‌شود، نه Request Body.
  • Secretها فقط در Backend نگهداری می‌شوند.
  • خطاهای داخلی به Client نمایش داده نمی‌شوند.
  • Routerها براساس Domain تقسیم شده‌اند.
  • Business Logic در Serviceهای تست‌پذیر قرار دارد.
  • فهرست‌ها Pagination دارند.
  • Batch Size محدود شده است.
  • Timeout درخواست‌های خارجی تعیین شده است.
  • پاسخ API خارجی اعتبارسنجی می‌شود.
  • Queryهای دیتابیس اندازه‌گیری و بهینه شده‌اند.
  • Procedureها با createCaller و Integration Test بررسی می‌شوند.
  • نسخه Client و Server هنگام Deployment سازگار است.
  • برای API عمومی چندزبانه، REST یا قرارداد مستقل وجود دارد.
  • Logging شامل Request ID و زمان اجرا است.
  • داده حساس و متن خصوصی کاربران بدون سیاست مشخص Log نمی‌شود.

پرسش‌های متداول

tRPC چیست؟

tRPC فریم‌ورکی برای ساخت APIهای End-to-End Type-Safe میان Backend و Frontend TypeScript است. Client نوع Procedureها، ورودی‌ها و خروجی‌ها را مستقیماً از Router سمت Server دریافت می‌کند.

آیا tRPC جایگزین REST است؟

در بعضی برنامه‌های Full-Stack TypeScript می‌تواند جایگزین REST داخلی شود، اما برای API عمومی، چندزبانه و قابل استفاده توسط مشتریان خارجی، REST و OpenAPI همچنان انتخاب مناسبی هستند.

آیا tRPC جایگزین GraphQL است؟

tRPC و GraphQL مسائل مشابهی را با روش‌های متفاوت حل می‌کنند. GraphQL دارای Schema مستقل و امکان انتخاب فیلد توسط Client است؛ tRPC بر Type Inference مستقیم در پروژه TypeScript تمرکز دارد.

آیا tRPC فقط با React کار می‌کند؟

خیر. Client استاندارد tRPC را می‌توان بدون React استفاده کرد. ادغام رسمی با TanStack React Query تجربه توسعه React را بهتر می‌کند.

آیا tRPC به Zod نیاز دارد؟

اجباری نیست، اما tRPC استفاده از Validator را توصیه می‌کند و Zod یکی از انتخاب‌های اصلی آن است. بدون Validation، داده Runtime همچنان ممکن است نامعتبر باشد.

آیا tRPC به Code Generation نیاز دارد؟

معمولاً خیر. Client Type مربوط به AppRouter را مستقیماً استفاده می‌کند.

آیا tRPC از Express پشتیبانی می‌کند؟

بله. Adapter رسمی Express از مسیر @trpc/server/adapters/express در دسترس است.

آیا tRPC با Prisma کار می‌کند؟

بله. می‌توان Prisma Client را در Context قرار داد و در Procedureها Queryهای Type-Safe اجرا کرد.

آیا می‌توان با tRPC API هوش مصنوعی ساخت؟

بله. Procedure سمت Server می‌تواند درخواست را به API هوش مصنوعی ارسال کند و نتیجه Type-Safe را به React برگرداند. کلید API باید در Backend باقی بماند.

آیا tRPC از Streaming پشتیبانی می‌کند؟

نسخه‌های جدید tRPC از Streaming و Subscription از طریق روش‌هایی مانند SSE و WebSocket پشتیبانی می‌کنند. انتخاب روش باید براساس نیاز برنامه انجام شود.

آیا tRPC برای Microservices مناسب است؟

برای ارتباط Frontend و Backend TypeScript یا سرویس‌های تحت کنترل یک تیم مناسب است. برای مرز عمومی میان سرویس‌های مستقل، قراردادهای استاندارد مانند HTTP/OpenAPI، GraphQL، gRPC یا پیام‌رسانی رویدادمحور ممکن است مناسب‌تر باشند.

آیا Type Safety مانع تمام خطاهای API می‌شود؟

خیر. Type Safety خطاهای قراردادی زیادی را کاهش می‌دهد، اما جایگزین Runtime Validation، تست، مدیریت خطا، مجوز دسترسی، Monitoring و بررسی منطق برنامه نیست.

جمع‌بندی

tRPC راهی ساده و قدرتمند برای ساخت API کاملاً Type-Safe در پروژه‌هایی است که Frontend و Backend هر دو با TypeScript توسعه داده می‌شوند.

با تعریف یک AppRouter در Backend، Client می‌تواند بدون Typeهای تکراری یا Code Generation، Procedureها و ورودی و خروجی آن‌ها را بشناسد. ترکیب tRPC با Zod، React، TanStack Query، Prisma و Node.js یک Stack منسجم برای ساخت برنامه‌های Full-Stack ایجاد می‌کند.

بهترین کاربرد tRPC معمولاً در شرایط زیر است:

  • Frontend و Backend هر دو TypeScript هستند.
  • کدها در Monorepo یا مخزن‌های هماهنگ نگهداری می‌شوند.
  • Client و Server تحت کنترل یک تیم هستند.
  • سرعت توسعه و Refactoring اهمیت دارد.
  • API عمومی چندزبانه هدف اصلی نیست.

برای برنامه‌های هوش مصنوعی نیز می‌توان Procedureهای tRPC را به‌عنوان لایه امن میان React و API درواره استفاده کرد. Frontend فقط Procedure Type-Safe را فراخوانی می‌کند، درحالی‌که کلید API، اعتبارسنجی پاسخ، Timeout و مدیریت خطا در Backend باقی می‌مانند.

اگر محصول شما علاوه بر Dashboard داخلی، API عمومی برای توسعه‌دهندگان مختلف ارائه می‌کند، استفاده ترکیبی منطقی‌تر است: tRPC برای رابط داخلی TypeScript و REST یا OpenAPI برای قرارداد عمومی.

منابع تکمیلی

مقالات مرتبط

برای مطالعه شرایط استفاده و محدودیت‌های مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.

Read more

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی با ترکیب گردش‌کارهای خودکار و مدل‌های هوش مصنوعی، پردازش متن، دسته‌بندی، استخراج اطلاعات و تصمیم‌های پیشنهادی را خودکار می‌کند. در این راهنما، معماری و ساخت نمونه عملی آن با API درواره را می‌آموزید.

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce شیوه‌ای جدید برای خرید اینترنتی است که در آن ایجنت هوش مصنوعی می‌تواند نیاز کاربر را بفهمد، محصولات را جست‌وجو و مقایسه کند و فرایند خرید را پیش ببرد. در این راهنما با معماری، UCP، ACP و پیاده‌سازی آن با API درواره آشنا می‌شوید.