IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar19/36-dars20 daqiqa
Mundarija (23)

GraphQL va tRPC frontendda: so'rov tili va end-to-end type-safe chaqiruvlar

Qisqacha: REST'da server qaysi manzil qanday ma'lumot berishini hal qiladi. GraphQL da mijoz bitta manzilga so'rov matnini yuboradi va faqat kerakli maydonlarni oladi. tRPC da esa server va klient bitta TypeScript loyihasida: klient server funksiyalarini oddiy funksiya kabi chaqiradi va turlar avtomatik mos keladi. Ikkalasi ham serverga bog'liq tanlov — server GraphQL yoki tRPC bo'lmasa, frontendda ularni ishlatib bo'lmaydi.

Bu darsda

  • GraphQL so'rovi qanday ko'rinishini va tarmoqda nima ketishini ko'rasiz: query, mutation, variables.
  • GraphQL xatosi HTTP 200 bilan kelishini va uni qanday ushlashni bilasiz.
  • Apollo Client, urql va "fetch + TanStack Query" yo'llarini solishtirasiz; graphql-codegen nima berishini bilasiz.
  • tRPC g'oyasini — server turini klientda ishlatishni — o'z qo'lingiz bilan yozib ko'rasiz; oRPC bilan farqini bilasiz.
  • Tajriba raqamlari bilan qaysi vaziyatda qaysi biri to'g'ri tanlov ekanini aytasiz.

Oldin bilishingiz kerak: TanStack Query III: mutatsiyalar, API qatlami va type-safe kontrakt, fetch bilan ma'lumot yuborish, Utility types.

1. Nega bu kerak?

«Bahor» mobil ilovasi bosh sahifasida taomlarning faqat nomi va narxi kerak. REST API esa /api/dishes da har taom uchun hammasini beradi: nom, narx, uzun retsept, tarkib, rasm manzillari, sharhlar soni. Telefon har safar keraksiz kilobaytlarni yuklaydi. Bu — ortiqcha olish (over-fetching).

Teskari muammo ham bor. Taom sahifasida taom, uning oshpazi va oxirgi 3 ta sharh kerak. REST'da — uch so'rov: /dishes/1, /chefs/7, /dishes/1/reviews?limit=3. Bu — yetarlicha olmaslik (under-fetching): bitta ekran uchun bir nechta so'rov, ko'pincha sharshara bilan.

Uchinchi muammo — turlar. API qatlami darsida server javobining turini qo'lda yozdik va Zod bilan tekshirdik. Server maydon nomini o'zgartirsa, frontend buni faqat ishlaganda biladi.

O'xshatish: restoranda ovqat buyurtma qilish. REST — tayyor set menyu: "1-set" deysiz, ichida nima bo'lsa — hammasi keladi. GraphQL — "a la carte": "osh, faqat go'shtli qismi, salat va choy" — aynan shuni olasiz. tRPC esa — oshxona va zal bitta oila: ofitsiant oshpaz bilan bir tilda gaplashadi, tushunmovchilik bo'lmaydi.

2. GraphQL: mijoz nima kerakligini aytadi

2.1 So'rov — matn

GraphQL — API uchun so'rov tili (Facebook, 2015). Server bitta manzilda (odatda /graphql) turadi va sxema e'lon qiladi: qanday turlar va maydonlar bor. Mijoz so'rov matnini yuboradi:

graphql
query {
  dishes {
    name
    price
  }
}

Javob — aynan so'rov shaklida, faqat so'ralgan maydonlar bilan:

json
{
  "data": {
    "dishes": [
      { "name": "Osh", "price": 35000 },
      { "name": "Manti", "price": 30000 }
    ]
  }
}

Tarmoqda bu — oddiy POST /graphql, tanasida JSON: { "query": "…", "variables": { … } }. Hech qanday sehr yo'q — fetch bilan ma'lumot yuborish darsidagi POST.

"Faqat so'ralgan maydonlar" qanday ishlashini ko'ramiz. Bu soxta server juda soddalashtirilgan: haqiqiy GraphQL server sxema va to'liq tahlilchi bilan ishlaydi. Bu esa faqat { dishes { … } } shaklini tushunadi:

js
const DISHES = [
  { id: 1, name: "Osh", price: 35000, recipe: "Uzun retsept…" },
  { id: 2, name: "Manti", price: 30000, recipe: "Uzun retsept…" },
];

// Soxta GraphQL server: so'ralgan maydonlarni ajratib,
// faqat ularni beradi
function graphqlServer(body) {
  const { query } = JSON.parse(body);
  const inner = query.match(/dishes\s*{([^}]*)}/)[1];
  const fields = inner.trim().split(/\s+/);
  const dishes = DISHES.map((dish) =>
    Object.fromEntries(fields.map((f) => [f, dish[f]])),
  );
  return JSON.stringify({ data: { dishes } });
}

const body = JSON.stringify({ query: "{ dishes { name price } }" });
console.log("so'rov:", body);
console.log("javob:", graphqlServer(body));

const small = JSON.stringify({ query: "{ dishes { name } }" });
console.log("javob:", graphqlServer(small));

Konsolda:

text
so'rov: {"query":"{ dishes { name price } }"}
javob: {"data":{"dishes":[{"name":"Osh","price":35000},{"name":"Manti","price":30000}]}}
javob: {"data":{"dishes":[{"name":"Osh"},{"name":"Manti"}]}}

Bitta server, bitta manzil — lekin ikki xil so'rov ikki xil hajmdagi javob oldi. Retsept (recipe) hech qachon ketmadi, chunki uni hech kim so'ramadi.

2.2 Ichma-ich ma'lumot va variables

GraphQL'ning ikkinchi kuchi — bog'langan ma'lumotni bitta so'rovda olish. «Nega bu kerak?» dagi taom sahifasi:

graphql
query DishPage($id: ID!) {
  dish(id: $id) {
    name
    price
    chef {
      name
    }
    reviews(last: 3) {
      text
      rating
    }
  }
}

Uch REST so'rovi o'rniga bitta. DishPage — so'rov nomi (loglarda va DevTools'da ko'rinadi). $id: ID! — o'zgaruvchi (variable): qiymat matnga yopishtirilmaydi, alohida variables maydonida ketadi: { "query": "…", "variables": { "id": "1" } }. ! — "majburiy".

Diqqat: Qiymatni so'rov matniga satr birlashtirish bilan qo'ymang: `dish(id: "${id}")`. Bu SQL injection'ning GraphQL'dagi ko'rinishi: foydalanuvchi id ga qo'shtirnoq va boshqa so'rov qismini yozib, so'rovni o'zgartirishi mumkin. Doim variables — server ularni turi bo'yicha tekshiradi.

Yozish — mutation:

graphql
mutation AddReview($dishId: ID!, $text: String!) {
  addReview(dishId: $dishId, text: $text) {
    id
    text
  }
}

Mutatsiya ham javobda kerakli maydonlarni so'raydi. Yangi sharhning id sini darhol olib, setQueryData bilan keshga qo'yish mumkin.

2.3 Xato — HTTP 200 bilan

GraphQL'ning eng ko'p uchraydigan tuzog'i. REST'da xato — HTTP holati: 404, 500. GraphQL'da server so'rovni qabul qilgan bo'lsa, odatda 200 qaytaradi, xato esa javob ichida — errors massivida. Ba'zan data ning bir qismi bor, bir qismi xato.

if (!res.ok) throw (TanStack Query I) bu yerda yetmaydi. errors ni ham tekshirish kerak. GraphQL'ni TanStack Query bilan ishlatamiz — Query uchun bu oddiy queryFn. Natija oynasida tarmoq yo'q, shuning uchun gqlFetch — soxta server:

jsx
import { useState } from "react";
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from "@tanstack/react-query";

const queryClient = new QueryClient({
  defaultOptions: { queries: { retry: false } },
});

const DISHES = { 1: "Osh", 2: "Manti" };

// Soxta /graphql: HTTP doim 200, xato — "errors" ichida
function gqlFetch(query, variables) {
  console.log("POST /graphql", JSON.stringify(variables));
  const name = DISHES[variables.id];
  const body = name
    ? { data: { dish: { name } } }
    : {
        data: { dish: null },
        errors: [{ message: `Dish ${variables.id} not found` }],
      };
  return Promise.resolve({ ok: true, status: 200, json: () => body });
}

const DISH_QUERY = `query Dish($id: ID!) { dish(id: $id) { name } }`;

async function graphqlRequest(query, variables) {
  const res = await gqlFetch(query, variables);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const body = await res.json();
  if (body.errors?.length) throw new Error(body.errors[0].message);
  return body.data;
}

function Dish({ id }) {
  const { isPending, isError, error, data } = useQuery({
    queryKey: ["dish", id],
    queryFn: () => graphqlRequest(DISH_QUERY, { id }),
  });
  if (isPending) return <p>Yuklanmoqda…</p>;
  if (isError) return <p>Xato: {error.message}</p>;
  return <p>Taom: {data.dish.name}</p>;
}

function Page() {
  const [id, setId] = useState(1);
  return (
    <>
      <button onClick={() => setId(2)}>Manti</button>{" "}
      <button onClick={() => setId(3)}>Shashlik</button>
      <Dish id={id} />
    </>
  );
}

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Page />
    </QueryClientProvider>
  );
}

Konsolda:

text
POST /graphql {"id":1}
POST /graphql {"id":2}
POST /graphql {"id":3}

Ekranda:

text
Manti Shashlik
Xato: Dish 3 not found

Uchala javob ham status: 200. Uchinchisida data.dish — null va errors da sabab. graphqlRequest dagi if (body.errors?.length) throw qatori bo'lmasa, Query buni muvaffaqiyat deb keshga yozardi va data.dish.name — Cannot read properties of null bilan oq ekran.

Shu misolda yana bir narsa ko'rinadi: GraphQL bilan ishlash uchun maxsus kutubxona shart emas. Bitta graphqlRequest funksiyasi va Query — ko'p loyihalar aynan shunday ishlaydi (ko'pincha graphql-request kabi kichik yordamchi bilan).

Tekshirib ko'ring: Server { "data": { "dish": null } } qaytardi, errors yo'q. Bu xatomi?

Javob

GraphQL nuqtai nazaridan — yo'q: so'rov to'g'ri, shunchaki bunday taom topilmadi (sxemada dish maydoni null bo'lishi mumkin deb e'lon qilingan). Bu REST'dagi 404 ga yaqin holat, lekin xato sifatida emas, bo'sh natija sifatida keladi. Komponent uni alohida ko'rsatishi kerak: if (data.dish === null) return <p>Taom topilmadi</p>.

3. GraphQL klientlari

3.1 urql va Apollo Client

Maxsus GraphQL klientlari so'rov yuborishdan ko'proq narsa qiladi: o'z keshi, React hook'lari, obunalar (subscription — GraphQL'ning real-time qismi).

urql — yengil klient. Tajribada (tajriba/variantlar/api-urql.tsx, kanondan; urql 5.0.4; uzun qatorlar telefonga sig'ishi uchun bo'lingan):

tsx
// api-urql.tsx — GraphQL klienti urql 5 (19/#19): so'rov — gql
// hujjati, javob — faqat so'ralgan maydonlar. Server GraphQL
// bo'lishi shart («Bahor» — REST); turlar uchun alohida
// generator (graphql-codegen) kerak
import {
  Client,
  cacheExchange,
  fetchExchange,
  gql,
  Provider,
  useMutation,
  useQuery,
} from "urql";
import { ishgaTushir } from "./_umumiy.tsx";

const mijoz = new Client({
  url: "/graphql",
  exchanges: [cacheExchange, fetchExchange],
});
const ROYXAT = gql`
  query {
    vazifalar {
      id
      matn
    }
  }
`;
const QOSH = gql`
  mutation ($matn: String!) {
    qosh(matn: $matn) {
      id
    }
  }
`;

function Ilova() {
  const [{ data, fetching }] = useQuery<{
    vazifalar: { id: number; matn: string }[];
  }>({ query: ROYXAT });
  const [, qosh] = useMutation(QOSH);
  return (
    <>
      <button onClick={() => void qosh({ matn: "Yangi" })}>
        Qo'shish
      </button>
      <p>{fetching ? "Yuklanmoqda…" : ""}</p>
      <ul>
        {data?.vazifalar.map((v) => (
          <li key={v.id}>{v.matn}</li>
        ))}
      </ul>
    </>
  );
}

Tanish shakl: useQuery va useMutation — faqat bu urql'niki, TanStack'niki emas. Client — QueryClient o'rni, Provider — QueryClientProvider o'rni. gql — so'rov matnini tahlil qilib, hujjatga aylantiradigan teg (Tagged template darsidagi sintaksis). Exchange — so'rov o'tadigan "quvur" bosqichlari: kesh, keyin tarmoq.

Apollo Client — eng mashhur va eng to'liq GraphQL klienti. Uning kuchi — normallashtirilgan kesh: har obyekt __typename va id bo'yicha bitta joyda saqlanadi. Taom nomi bir so'rovda o'zgarsa, shu taomni ko'rsatadigan hamma so'rov avtomatik yangilanadi — invalidatsiyasiz. Narxi — hajm va murakkablik: kesh qoidalari, typePolicies, to'plamda katta qism. Apollo kursning tajribasida o'lchanmadi, shuning uchun raqam bermaymiz.

3.2 Turlar: graphql-codegen

urql misolida useQuery<{ vazifalar: { id: number; matn: string }[] }> turini qo'lda yozdik. Server sxemasi o'zgarsa — tur eskiradi va hech kim aytmaydi. Kanon izohi buni ochiq aytadi: "turlar uchun alohida generator (graphql-codegen) kerak".

graphql-codegen — server sxemasi va loyihadagi so'rovlardan TypeScript turlarini yasaydigan vosita. Ish tartibi:

flowchart LR
  S["Server sxemasi"] --> G["graphql-codegen"]
  Q["Loyihadagi so'rovlar"] --> G
  G --> T["TS turlari (fayl)"]
  T --> C["Komponentlar"]

Natijada har so'rovning javob turi aniq va server bilan mos. Sxema o'zgarsa, codegen'ni qayta ishga tushirasiz va tsc buzilgan joylarni ko'rsatadi. Odatda npm run codegen skripti va CI'da tekshiruv. Bu — API qatlami dagi "qo'lda yozilgan tur + Zod" ning avtomatik varianti.

3.3 Qaysi biri

fetch + TanStack Query urql Apollo Client
kesh so'rov bo'yicha (kalit) so'rov bo'yicha (+ graphcache) normallashtirilgan
hajm eng kichik o'rta (+10.4 KB tajribada) eng katta
subscription qo'lda bor bor
qachon GraphQL — bir nechta API'dan biri GraphQL asosiy, oddiy kesh yetadi katta GraphQL ilova, murakkab kesh

4. tRPC: server funksiyasini chaqirish

4.1 G'oya

GraphQL ham, REST ham tilga bog'liq emas: server Go'da, klient TypeScript'da bo'lishi mumkin. Shuning uchun ular orasida "shartnoma" kerak — sxema yoki hujjat — va turlar alohida yasaladi.

Lekin ko'p loyihalarda server ham TypeScript'da (Node, Express kabi), va ikkalasi bitta repo'da (monorepo). Unda savol: server funksiyasining turini klient to'g'ridan-to'g'ri ishlata olmaydimi? tRPC (TypeScript Remote Procedure Call) aynan shu: server protseduralar (funksiyalar) e'lon qiladi, klient ularning turini import type bilan oladi. Kod emas, faqat tur — server kodi brauzer to'plamiga tushmaydi.

4.2 O'z qo'limiz bilan: tur chegaradan qanday o'tadi

tRPC'ning ichki mexanizmini kutubxonasiz ko'ramiz. Uch fayl: server, klient va ishlatish. Bu — soddalashtirilgan model, tRPC'ning o'zi emas: haqiqiy tRPC'da tarmoq, validatsiya va boshqa ko'p narsa bor.

Server — oddiy funksiyalar obyekti. Oxirgi qator muhim: typeof router — obyektning turi:

ts
// fayl: rpc/server.ts — server: protseduralar (soddalashtirilgan)
export type Dish = { id: number; name: string; price: number };

const dishes: Dish[] = [
  { id: 1, name: "Osh", price: 35000 },
  { id: 2, name: "Manti", price: 30000 },
];

export const router = {
  dishById(id: number): Dish | undefined {
    return dishes.find((d) => d.id === id);
  },
  cheaperThan(max: number): string[] {
    return dishes.filter((d) => d.price < max).map((d) => d.name);
  },
};

export type AppRouter = typeof router;

Klient — server kodini emas, faqat turini import qiladi. Parameters va ReturnType (Utility types) bilan har protseduraning kirish va chiqish turini oladi:

ts
// fayl: rpc/client.ts — klient: serverdan faqat TURNI oladi
import type { AppRouter } from "./server.ts";

type Procedure = keyof AppRouter;
type Input<P extends Procedure> = Parameters<AppRouter[P]>[0];
type Output<P extends Procedure> = ReturnType<AppRouter[P]>;

// tRPC'da transport — HTTP so'rov; bu yerda tashqaridan beriladi
export function createClient(
  transport: (name: string, input: unknown) => Promise<unknown>,
) {
  return <P extends Procedure>(name: P, input: Input<P>) =>
    transport(name, input) as Promise<Output<P>>;
}

Ishlatish. Tarmoq yo'q, shuning uchun "transport" server funksiyasini to'g'ridan chaqiradi — haqiqiy tRPC'da bu yerda fetch bo'ladi:

ts
// fayl: rpc/main.ts — ishlatish
import { createClient } from "./client.ts";
import { router } from "./server.ts";

const procedures: Record<string, (input: never) => unknown> = router;

const call = createClient(async (name, input) => {
  console.log("so'rov:", name, JSON.stringify(input));
  return procedures[name]?.(input as never);
});

const dish = await call("dishById", 1);
console.log(dish?.name, dish?.price);
const cheap = await call("cheaperThan", 32000);
console.log(cheap.join(", "));

Konsolda:

text
so'rov: dishById 1
Osh 35000
so'rov: cheaperThan 32000
Manti

dish turi — Dish | undefined, cheap niki — string[]. Biz klientda bu turlarni hech qayerda yozmadik — ular serverdan keldi. Endi xato chaqiruvlar:

ts
// fayl: rpc/xato.ts — xato chaqiruvlar
import { createClient } from "./client.ts";

const call = createClient(async () => undefined);
await call("dishById", "1");
await call("dishByName", 1);
const dish = await call("dishById", 1);
console.log(dish.name);

Xato:

text
TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
TS2345: Argument of type '"dishByName"' is not assignable to parameter of type '"cheaperThan" | "dishById"'.
TS18048: 'dish' is possibly 'undefined'.

Uchala xato kompilyatsiyada ushlandi, kod ishga tushmasdan:

  1. "1" — satr, server number kutadi.
  2. "dishByName" — bunday protsedura yo'q; xabarda bor nomlar ro'yxati.
  3. dish — undefined bo'lishi mumkin: server "topilmasa undefined" deb e'lon qilgan.

Endi tasavvur qiling: serverda dishById ning parametri slug: string ga o'zgartirildi. Klientdagi har chaqiruv tsc da qizil bo'ladi — deploy'dan oldin. REST va qo'lda yozilgan turlarda bu xato foydalanuvchida, ishlaganda chiqardi.

4.3 Haqiqiy tRPC

Tajribadagi server va klient (tajriba/variantlar/_trpc-server.ts, api-trpc.tsx, kanondan; tRPC 11.19.0; qatorlar bo'lingan):

ts
// _trpc-server.ts — tRPC server router'i (FAQAT turi uchun:
// klient "import type" qiladi, server kodi bundle'ga
// tushmaydi). Haqiqiy server — 25-qismda (Node). Bu yerda —
// kontrakt qanday ko'rinishi
import { initTRPC } from "@trpc/server";

const t = initTRPC.create();
type Vazifa = { id: number; matn: string; bajarildi: boolean };
const VAZIFALAR: Vazifa[] = [];

export const appRouter = t.router({
  vazifalar: t.procedure.query(() => VAZIFALAR),
  qosh: t.procedure
    .input((x: unknown) => {
      if (typeof x !== "string" || x.trim() === "") {
        throw new Error("matn");
      }
      return x;
    })
    .mutation(({ input }) => {
      const v = {
        id: VAZIFALAR.length + 1,
        matn: input,
        bajarildi: false,
      };
      VAZIFALAR.push(v);
      return v;
    }),
});
export type AppRouter = typeof appRouter;
tsx
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "./_trpc-server.ts";

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

// komponentda:
void trpc.vazifalar.query().then(setVazifalar);
void trpc.qosh
  .mutate("Yangi")
  .then((v) => setVazifalar((x) => [...x, v]));

O'z modelimiz bilan farqlar:

  • query va mutation — GraphQL'dagi kabi o'qish va yozish ajratiladi.
  • .input(...) — server kirishni ishlaganda ham tekshiradi. Tur faqat kompilyatsiyada himoya qiladi; tarmoqdan esa istalgan narsa kelishi mumkin. Haqiqiy loyihada bu yerda Zod sxemasi: .input(z.string().min(1)).
  • httpBatchLink — bir vaqtda chaqirilgan bir nechta protsedurani bitta HTTP so'rovga yig'adi.

TanStack Query bilan ham ishlaydi: @trpc/tanstack-react-query paketi har protsedura uchun queryOptions yasaydi — useQuery(trpc.vazifalar.queryOptions()). Ya'ni TanStack Query II dagi queryOptions naqshi, faqat kalit va tur avtomatik.

oRPC — xuddi shu g'oyaning yangiroq muqobili. Asosiy farqi — protseduralardan OpenAPI hujjati yasay oladi. Ya'ni ichkarida tRPC kabi type-safe, tashqaridan esa oddiy REST API: mobil ilova yoki boshqa tilda yozilgan mijoz ham ishlata oladi. tRPC'da bu qiyinroq.

5. Tajriba: «Bahor» uchun qaysi biri?

Bu darsda vazifalar kodi o'zgarmaydi. Kanon ikkala klientni o'lchadi va kiritmadi.

Biz sinab ko'rdik. Tajriba to'plamida o'sha kichik vazifa (ro'yxat va qo'shish) — qo'lda fetch, tRPC klienti va urql bilan. Build hajmi (gzip, React bilan birga, natijalar/tajriba/hajm.txt): qo'lda — 68 001 B, tRPC — 73 183 B, urql — 78 354 B. Qo'lda yozilganga nisbatan:

API klientining narxi (qo'lda fetch'ga qo'shimcha, gzip)
  • qo'lda fetch0 KB
  • tRPC klient 11.19.05,2 KB
  • TanStack Query 5.104.1taqqoslash uchun10,3 KB
  • urql 5.0.410,4 KB

Manba: sinov/19/natijalar/tajriba/hajm.txt (vite build, gzip -9, 2026-10-13)

Tajribadagi tur tekshiruvi (npx tsc -p tsconfig.json) — toza (natijalar/tajriba/tip.txt). Bitta izoh bilan: @trpc/server ning tur fayllari @types/node ni talab qilgani uchun skipLibCheck yoqildi.

Nega kanonga kirmadi. «Bahor» mashq serveri — REST. Klient kutubxonasi serverni o'zgartirmaydi:

  • GraphQL — server GraphQL bo'lsa. Bizniki emas.
  • tRPC — server ham TypeScript va shu repo'da bo'lsa. vazifalar hozircha faqat frontend; o'z backend'imiz 25-qismdan boshlanadi. Shunda ham tanlov — server bilan birga qilinadi (tRPC, GraphQL asoslari darslarida).

Hozirgi vazifalar dagi api.ts + Zod (API qatlami) — REST uchun type-safe kontraktning qo'lda varianti. tRPC buni avtomatlashtiradi, lekin faqat server ham sizniki bo'lsa.

6. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
GraphQL xatosi "muvaffaqiyat" bo'lib keshda HTTP 200, xato errors ichida if (body.errors?.length) throw
Cannot read properties of null data.dish — null (topilmadi) null holatini alohida ko'rsating
so'rov matniga qiymat qo'shilgan satr birlashtirish variables
GraphQL javobi turi eskirgan tur qo'lda yozilgan graphql-codegen
tRPC turi any import type noto'g'ri yo'ldan yoki AppRouter eksport qilinmagan export type AppRouter = typeof appRouter
brauzer to'plamida server kodi import type o'rniga import faqat import type
tRPC'da tur to'g'ri, lekin server yiqildi kirish ishlaganda tekshirilmagan .input(zodSxema)

7. Mashqlar

Mashqlar kurs/mashqlar/19/19-graphql-trpc/ da.

1-mashq (oson): REST, GraphQL yoki tRPC?

  • Backend Go'da, mobil va veb ilova bir xil API ishlatadi, har ekran turli maydonlar so'raydi →
  • Next.js monorepo: frontend va backend bitta TypeScript loyihada, bitta jamoa →
  • Ochiq API, uni boshqa kompaniyalar ham ishlatadi, oddiy keshlash (CDN) muhim →
Yechim
  1. GraphQL — tilga bog'liq emas, har mijoz kerakli maydonlarni so'raydi.
  2. tRPC — server va klient bitta TS loyihada, turlar avtomatik.
  3. REST — eng keng tarqalgan, HTTP keshlash (GET + Cache-Control) oddiy ishlaydi. GraphQL'da hamma so'rov bitta POST /graphql, CDN keshi qiyinroq.

2-mashq (o'rta): errors ni ushlang

graphqlRequest faqat HTTP holatini tekshiradi. Server 200 bilan errors qaytarsa, ekranda Taom: (bo'sh) chiqadi. Bitta qator qo'shing: errors bo'lsa — birinchi xabar bilan xato tashlansin.

jsx
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from "@tanstack/react-query";

const queryClient = new QueryClient({
  defaultOptions: { queries: { retry: false } },
});

function gqlFetch() {
  const body = {
    data: { dish: null },
    errors: [{ message: "Ruxsat yo'q" }],
  };
  return Promise.resolve({ ok: true, status: 200, json: () => body });
}

async function graphqlRequest(query, variables) {
  const res = await gqlFetch(query, variables);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const body = await res.json();
  return body.data;
}

function Dish() {
  const { isPending, isError, error, data } = useQuery({
    queryKey: ["dish", 1],
    queryFn: () =>
      graphqlRequest("query { dish(id: 1) { name } }", {}),
  });
  if (isPending) return <p>Yuklanmoqda…</p>;
  if (isError) return <p>Xato: {error.message}</p>;
  return <p>Taom: {data.dish?.name}</p>;
}

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Dish />
    </QueryClientProvider>
  );
}

Ekranda:

text
Taom:
Yechim
jsx
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from "@tanstack/react-query";

const queryClient = new QueryClient({
  defaultOptions: { queries: { retry: false } },
});

function gqlFetch() {
  const body = {
    data: { dish: null },
    errors: [{ message: "Ruxsat yo'q" }],
  };
  return Promise.resolve({ ok: true, status: 200, json: () => body });
}

async function graphqlRequest(query, variables) {
  const res = await gqlFetch(query, variables);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const body = await res.json();
  if (body.errors?.length) throw new Error(body.errors[0].message);
  return body.data;
}

function Dish() {
  const { isPending, isError, error, data } = useQuery({
    queryKey: ["dish", 1],
    queryFn: () =>
      graphqlRequest("query { dish(id: 1) { name } }", {}),
  });
  if (isPending) return <p>Yuklanmoqda…</p>;
  if (isError) return <p>Xato: {error.message}</p>;
  return <p>Taom: {data.dish?.name}</p>;
}

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Dish />
    </QueryClientProvider>
  );
}

Ekranda:

text
Xato: Ruxsat yo'q

errors?.length — errors umuman bo'lmasa ham (undefined) ishlaydi (??, ?. va mantiqiy tayinlash). Endi xato Query'ning isError iga tushdi va foydalanuvchi sababni ko'rdi.

3-mashq (qiyin): o'z RPC klientingizga protsedura qo'shing

«O'z qo'limiz bilan» bo'limidagi modelga yangi protsedura qo'shing va ishlating:

  1. Serverga dishCount(): number — taomlar soni. (Argumentsiz protseduraga klient undefined beradi.)
  2. Klientda await call("dishCount", undefined) va natijani console.log bilan chiqaring: Taomlar: 2.
  3. Klient kodiga (client.ts) tegmang — tur o'zi kelishi kerak.

Ishora: server faylini yangi fayl: bloki bilan to'liq qayta yozing (shu yo'l bilan blok eskisini almashtiradi). Parameters<() => number>[0] — undefined.

Yechim
ts
// fayl: rpc/server.ts — server: yangi protsedura bilan
export type Dish = { id: number; name: string; price: number };

const dishes: Dish[] = [
  { id: 1, name: "Osh", price: 35000 },
  { id: 2, name: "Manti", price: 30000 },
];

export const router = {
  dishById(id: number): Dish | undefined {
    return dishes.find((d) => d.id === id);
  },
  cheaperThan(max: number): string[] {
    return dishes.filter((d) => d.price < max).map((d) => d.name);
  },
  dishCount(): number {
    return dishes.length;
  },
};

export type AppRouter = typeof router;
ts
// fayl: rpc/count.ts — yangi protsedurani ishlatish
import { createClient } from "./client.ts";
import { router } from "./server.ts";

const procedures: Record<string, (input: never) => unknown> = router;

const call = createClient(async (name, input) =>
  procedures[name]?.(input as never),
);

const count = await call("dishCount", undefined);
console.log(`Taomlar: ${count}`);

Konsolda:

text
Taomlar: 2

client.ts o'zgarmadi, lekin call("dishCount", …) darhol paydo bo'ldi va count turi — number. Server turi o'zgardi — klient turi o'zi ergashdi. tRPC'ning butun g'oyasi shu. undefined o'rniga 5 yozib ko'ring — tsc xato beradi: protsedura argument kutmaydi.

8. Real ishda

  • GraphQL — GitHub API v4, Shopify, ko'p yirik mahsulotlar. Katta jamoalarda frontend backend'ni kutmasdan kerakli maydonlarni so'raydi. Narxi — serverda murakkablik: N+1 so'rovlar, so'rov chuqurligini cheklash, keshlash.
  • tRPC — Next.js + TypeScript startaplarida mashhur (T3 Stack). Bitta jamoa, bitta repo — tezlik.
  • REST + OpenAPI + codegen — eng keng tarqalgan "o'rta yo'l": REST API'dan OpenAPI hujjati, undan TS turlari. Klient tomoni xuddi codegen'dagi kabi.
  • Intervyu savollari: "GraphQL REST'dan nimasi bilan farq qiladi, kamchiliklari?", "over-fetching va under-fetching nima?", "GraphQL'da xatolar qanday qaytadi?", "tRPC qanday qilib type-safe, nega server kodi bundle'ga tushmaydi?".

Xulosa

  • GraphQL — bitta manzil, so'rov matni: mijoz kerakli maydonlarni oladi, bog'langan ma'lumotni bitta so'rovda. Qiymatlar — variables da.
  • GraphQL xatosi ko'pincha HTTP 200 bilan, errors ichida keladi — queryFn da tekshiring.
  • Klient: kichik loyihada fetch + TanStack Query, o'rtachada urql (+10.4 KB), murakkab keshda Apollo. Turlar — graphql-codegen.
  • tRPC — server ham TS va shu repo'da bo'lsa: import type bilan server turi klientga, xatolar tsc da. Ishlaganda kirishni .input() tekshiradi. oRPC — OpenAPI bilan.
  • Tanlov serverdan boshlanadi: «Bahor» — REST, shuning uchun vazifalar da ikkalasi ham yo'q.

Keyingi dars: State turlari xaritasi va vosita tanlash — local, URL, server keshi, forma va global store: har state o'z joyida; vazifalar holatining to'liq xaritasi.

Manbalar

  • GraphQL: Learn — graphql.org/learn (queries, mutations, variables, response format)
  • urql: Basics — nearform.com/open-source/urql/docs (v5)
  • GraphQL Code Generator — the-guild.dev/graphql/codegen
  • tRPC: Concepts, Quickstart — trpc.io/docs (v11)
  • oRPC — orpc.dev
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
GraphQL va tRPC frontendda: so'rov tili va end-to-end type-safe chaqiruvlar — IlmHamroh