IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar12/36-dars21 daqiqa
Mundarija (30)

API qatlami va type-safe kontrakt: Zod bilan server javobini tekshirish, xatolarni normallashtirish va OpenAPI

Qisqacha: Barcha server so'rovlari bitta API qatlamida yashaydi (api.ts): komponentlar fetch ni bilmaydi, api.royxat() ni chaqiradi. Server javobi aslida unknown — TypeScript turi (as Vazifa[]) uni tekshirmaydi. Shuning uchun har metod javobni sxema (Zod) bilan ish vaqtida tekshiradi va aniq tur qaytaradi; mos kelmasa — bitta turdagi xato (ApiXatosi, kod JAVOB) "qaysi maydon buzuq" degan matn bilan. Sxema ham tekshiruv, ham tur (z.infer) — bitta manba. Backend OpenAPI bersa, turlar va klient kodini generatsiya qilish mumkin: openapi-typescript, orval, Hey API.

Bu darsda

  • API qatlami nima uchun kerakligini va unda nima bo'lishini (manzil, sarlavhalar, xatolar, tekshiruv) bilasiz.
  • as bilan yozilgan tur ish vaqtida hech narsani tekshirmasligini jonli ko'rasiz.
  • Zod sxemasi bilan javobni tekshiradigan shartnoma() funksiyasini yozasiz va xato yo'lini ("narx: …") chiqarasiz.
  • Xatolarni bitta turga keltirasiz (tarmoq, 404, 500, buzuq javob) va UI uni qanday ko'rsatishini hal qilasiz.
  • OpenAPI asosida kod generatsiyasi (openapi-typescript, orval, Hey API) nima ekanini bilasiz; vazifalar ga Zod kontraktini qo'shasiz.

Oldin bilishingiz kerak: API klient qatlami, API ma'lumotlarini tiplash, Zod va turni sxemadan chiqarish, Server state vs client state.

1. Nega bu kerak?

Oldingi darsda server state — kesh ekanini ko'rdik. Keyingi darsdan boshlab uni TanStack Query boshqaradi. Lekin kesh ichiga nima tushadi? Server nima yuborsa — o'sha. Agar server noto'g'ri narsa yuborsa?

«Bahor» da shunday bo'ldi. Backend dasturchisi yangi versiyada narxni satr qilib yubora boshladi: "price": "35000". Frontend'da tur price: number deb yozilgan edi — TypeScript xursand. Savatda esa:

35 000 + 5 000 = 350005000 so'm

Raqamlar qo'shilmadi — satrlar ulandi. Hech qanday xato yo'q, mijoz esa 350 million so'mlik hisob ko'rdi. Muammo serverda edi, lekin uni frontend sezishi mumkin edi — chegarada, javob kelgan zahoti.

Hayotiy o'xshatish — oshxonaga keladigan mahsulot. Yaxshi oshpaz qopni ochmasdan qozonga to'kmaydi: avval qaraydi — "guruch shu, chiqitsiz, buzilmagan". Hujjatda "guruch, 10 kg" deb yozilgani (TypeScript turi) — va'da, xolos. Tekshiruv — eshik oldida (API qatlami), qozon oldida emas (komponent).

2. API qatlami: so'rovlar bitta joyda

2.1 Nima uchun bitta joy

API klient qatlami darsida fetch o'ramini yozgansiz. React ilovada bu yanada muhim: so'rov komponentda, loader'da, action'da, keyinchalik TanStack Query'da bo'ladi. Har biri fetch(…) ni o'zi yozsa:

  • manzil (https://…/api/vazifalar) o'nta joyda — o'zgarsa, o'nta tuzatish;
  • res.ok tekshiruvi qayerdadir unutiladi — 500 javobi "muvaffaqiyat" bo'lib o'tadi;
  • xatolar har xil: biri TypeError, biri matn, biri { error };
  • javob tekshiruvi — umuman yo'q.

API qatlami — frontend va backend orasidagi yagona eshik. vazifalar da u 13-qismdan beri bor (src/api.ts):

flowchart LR
  K["Komponent,<br/>loader, action"] -->|"api.royxat()"| A["api.ts"]
  A -->|"fetch + signal,<br/>vaqt chegarasi"| S[("Server")]
  S -->|"unknown JSON"| A
  A -->|"sxema tekshiruvi"| T{"mosmi?"}
  T -- ha --> K2["VazifaMalumoti[]"]
  T -- yo'q --> X["ApiXatosi (JAVOB)"]

Chap tomonda — faqat tiplangan qiymat yoki bitta turdagi xato. fetch, status kodlar, JSON — hammasi qatlam ichida.

2.2 Qatlam nima qiladi

Vazifa vazifalar dagi api.ts
asosiy manzil, sarlavhalar apiKlient({ asosiyUrl, kalit })
vaqt chegarasi va bekor qilish 8 s, AbortSignal (KUTISH_MS)
status → xato kodi 404 → TOPILMADI, 5xx → SERVER
tarmoq xatosi → xato kodi TARMOQ, VAQT, BEKOR
javob shakli tekshiruvi bugun qo'shiladi — Zod
metodlar royxat, qosh, yangila, ochir, almashtir

Axios bilan ham shunday qatlam yoziladi: axios.create({ baseURL }) va interceptor'lar (Axios). Vosita muhim emas — muhimi, qatlam bitta.

3. Tur — va'da, tekshiruv emas

3.1 as hech narsani tekshirmaydi

API ma'lumotlarini tiplash darsidan eslang: res.json() ning natijasi — any (aslida unknown). Ko'p kodda shunday yoziladi: const dish = (await res.json()) as Dish. TypeScript as ni ishonch deb qabul qiladi — tekshirmaydi. Natija oynasida o'sha voqea:

jsx
import { useEffect, useState } from "react";

// Server yangi versiyada narxni satr qilib yubordi
const fetchDish = () =>
  new Promise((resolve) =>
    setTimeout(() => resolve({ name: "Osh", price: "35000" }), 200),
  );

const DELIVERY = 5000;

export default function App() {
  const [dish, setDish] = useState(null);
  useEffect(() => {
    // TypeScript'da: setDish((await fetchDish()) as Dish)
    fetchDish().then(setDish);
  }, []);
  if (dish === null) return <p>Yuklanmoqda…</p>;
  return (
    <p>
      {dish.name}: jami {dish.price + DELIVERY} so'm
    </p>
  );
}

Ekranda:

text
Osh: jami 350005000 so'm

"35000" + 5000 — satr ulash (Yashirin coercion darsidagi + tuzog'i). Hech qanday xato, ogohlantirish yo'q — faqat noto'g'ri raqam. Eng xavfli xato turi: jim, ishonarli ko'rinadi va pul bilan bog'liq.

3.2 Chegarada tekshirish

Davosi — javobni chegarada (API qatlamida) ish vaqtida tekshirish. Qo'lda yozish mumkin (typeof price === "number") — vazifalar 15-qismdan beri shunday qilardi (serverRoyxatiniTekshir, 50 qatorga yaqin). Lekin har yangi maydon — yangi if, tur esa alohida yoziladi va tekshiruv bilan ajralib ketishi mumkin.

Zod bu ikkisini bitta qiladi: sxema ish vaqtida tekshiradi va TypeScript turini beradi. Natija oynasida zod/mini — vazifalar kanonidagi variant (nega "mini" — «Narxi: hajm, CSP va jitless» bo'limida):

jsx
import { z } from "zod/mini";

// sababi — «Narxi» bo'limida
z.config({ jitless: true });

const dishSchema = z.object({
  name: z.string().check(z.minLength(1, "nomi bo'sh")),
  price: z.number("narx son emas").check(
    z.int("narx butun emas"),
    z.positive("narx musbat emas"),
  ),
});

const good = dishSchema.safeParse({ name: "Osh", price: 35000 });
console.log(good.success, good.data);

const bad = dishSchema.safeParse({ name: "Osh", price: "35000" });
console.log(bad.success);
const [issue] = bad.error.issues;
console.log(issue.path.join("."), "—", issue.message);

const extra = dishSchema.parse({
  name: "Manti",
  price: 30000,
  admin: true,
});
console.log(extra);

export default function App() {
  return <p>Natija konsolda</p>;
}

Konsolda:

text
true {name: 'Osh', price: 35000}
false
price — narx son emas
{name: 'Manti', price: 30000}

To'rt qator — uch saboq:

  1. To'g'ri javob — success: true va data — tekshirilgan obyekt.
  2. Satr narx — success: false, issues[0] da yo'l (price) va xabar. "Qayerda buzuq" — aniq.
  3. Oxirgi qator: ortiqcha maydon (admin: true) — tashlab yuborildi. z.object faqat sxemadagi kalitlarni qoldiradi — oq ro'yxat. Bu «Hujumchi nigohi» da muhim.

safeParse — xato tashlamaydi, natija obyekt qaytaradi. parse — mos kelmasa ZodError tashlaydi. Zod natija oynasida xabarlarni faqat inglizcha beradi, shuning uchun o'zbekcha xabarni sxemada o'zimiz yozdik ("narx son emas"). Zod chuqurroq — Zod: coerce, refine, transform darsida.

Tekshirib ko'ring: Sxemada price: z.number(). Server "price": "35000" yuborsa, nega z.coerce.number() (satrni songa aylantirish) bilan "tuzatib" qo'ya qolmaymiz?

Javob

Chunki bu — kontrakt buzilishi, va uni yashirish keyin qimmatga tushadi. Bugun "35000", ertaga "35 000" yoki "35000.00 UZS" keladi — coerce NaN beradi yoki jimgina noto'g'ri son. Server javobida xato ko'rinsa, uni e'lon qilish kerak: frontend aniq xabar beradi, backend jamoasi tuzatadi. coerce URL va forma kabi "hamma narsa satr" bo'lgan joylar uchun (TanStack Router darsidagi ?sahifa=).

4. shartnoma() — har metod tekshirilgan tur qaytaradi

4.1 Bitta yordamchi

API qatlamidagi har metod uchun bitta qoida: so'rov → sxema → tekshirilgan qiymat yoki xato. Uni bitta funksiyaga yig'amiz. Natija oynasida vazifalar ning soddalashtirilgan nusxasi:

jsx
import { useEffect, useState } from "react";
import { z } from "zod/mini";

z.config({ jitless: true });

class ApiError extends Error {
  constructor(message, { code, cause }) {
    super(message, { cause });
    this.name = "ApiError";
    this.code = code;
  }
}

const dishSchema = z.object({
  id: z.int().check(z.positive("id musbat emas")),
  name: z.string().check(z.minLength(1, "nomi bo'sh")),
  price: z.number("narx son emas"),
});
const menuSchema = z.array(dishSchema);

// Javob shartnomaga mos kelmasa — bitta turdagi xato
function contract(schema, data) {
  const result = schema.safeParse(data);
  if (!result.success) {
    const [issue] = result.error.issues;
    const where = issue.path.join(".");
    throw new ApiError(
      `Server javobi kutilgan shaklda emas (${where}: ` +
        `${issue.message}).`,
      { code: "JAVOB", cause: result.error },
    );
  }
  return result.data;
}

// "Server" — ikkinchi taom buzuq
const serverJson = () =>
  new Promise((resolve) =>
    setTimeout(
      () =>
        resolve([
          { id: 1, name: "Osh", price: 35000 },
          { id: 2, name: "Manti", price: "30000" },
        ]),
      200,
    ),
  );

const api = {
  menu: async () => contract(menuSchema, await serverJson()),
};

export default function App() {
  const [state, setState] = useState({ status: "loading" });
  useEffect(() => {
    api
      .menu()
      .then((menu) => setState({ status: "ok", menu }))
      .catch((error) => {
        console.warn(error.code, error.message);
        setState({ status: "error", error });
      });
  }, []);
  if (state.status === "loading") return <p>Yuklanmoqda…</p>;
  if (state.status === "error") {
    return <p role="alert">Menyuni ochib bo'lmadi.</p>;
  }
  return <p>{state.menu.map((d) => d.name).join(", ")}</p>;
}

Konsolda:

text
JAVOB Server javobi kutilgan shaklda emas (1.price: narx son emas).

Ekranda:

text
Menyuni ochib bo'lmadi.

Yo'l 1.price — massivning ikkinchi elementi (indeks 1), price maydoni. Dasturchi konsolda aniq joyni ko'radi; foydalanuvchi esa tushunarli xabar oladi, 350 million so'mlik hisob emas. cause da asl Zod xatosi saqlanadi — Sentry kabi xato kuzatish xizmatiga to'liq ma'lumot ketadi.

Muhimi — komponent sxemani bilmaydi. U api.menu() dan yo tekshirilgan massiv, yo ApiError oladi. Tekshirishni unutib bo'lmaydi: tekshirilmagan qiymatni olishning boshqa yo'li yo'q.

4.2 Xato normallashtirish: bitta tur, bir nechta kod

ApiError ning code maydoni — UI uchun asosiy narsa. vazifalar dagi kodlar (turlar.ts, kanon):

Kod Qachon UI nima qiladi
TARMOQ internet yo'q, DNS, CORS "Serverga ulanib bo'lmadi", keshdagini ko'rsatadi
VAQT 8 s ichida javob yo'q o'sha, "qayta urinish"
BEKOR AbortController — foydalanuvchi ketdi hech narsa (xato emas)
TOPILMADI 404 "topilmadi" sahifasi
NOTOGRI 400, 422 forma xatolari
SERVER 5xx "serverda muammo, keyinroq"
JAVOB javob JSON emas yoki shartnomaga mos emas "saqlanmadi", konsolda maydon

Bu xato normallashtirish (error normalization): fetch ning TypeError i, HTTP status, JSON xatosi va Zod xatosi — hammasi bitta ApiXatosi turiga keltiriladi. Komponent if (xato.kod === "BEKOR") return; deb yozadi, instanceof TypeError va res.status ni bilmaydi. Data mode darsidagi loader aynan BEKOR ni ajratgan edi.

Maslahat: 401 (kirmagan) ko'pincha alohida kod oladi: API qatlami uni ko'rib, foydalanuvchini kirish sahifasiga yuboradi (himoyalangan marshrutlar). Bitta joyda — har sahifada emas. vazifalar da haqiqiy auth yo'q, shuning uchun bu kod hali yo'q — 28-qismda.

5. Backend bilan kontrakt: OpenAPI va kod generatsiyasi

5.1 Sxemani kim yozadi?

Hozir sxemani frontend yozdi — backend nima yuborishini "bilib". Backend o'zgarsa, frontend sxemasi eskiradi, va biz buni faqat ish vaqtida, JAVOB xatosi bilan bilamiz. Yaxshiroq yo'l — kontraktni bitta joyda yozish va ikkala tomonni undan olish.

OpenAPI — HTTP API'ni tavsiflaydigan standart: qaysi manzillar bor, qanday parametrlar, javob qanday shaklda (JSON yoki YAML fayl). Ko'p backend frameworklari uni koddan o'zi chiqaradi (NestJS, FastAPI — 25–30-qismlarda). Fayldan esa frontend uchun kod yasaladi:

Vosita (2026-10-14, npm view) Nima yasaydi
openapi-typescript 7.13.0 (2026-02-11) faqat turlar (paths["/vazifalar"]["get"]…); juftligi — openapi-fetch 0.17.0 kichik klienti
orval 8.42.0 (2026-10-13) turlar + tayyor klient funksiyalari, ixtiyoriy TanStack Query hook'lari va Zod sxemalari
Hey API (@hey-api/openapi-ts) 0.99.0 (2026-06-22) turlar, SDK, plaginlar orqali Zod va TanStack Query

Versiyalar faqat yo'nalish uchun: Hey API hali 0.x — kichik versiyalar orasida ham API o'zgarishi mumkin; orval'ning oxirgi relizi bir kunlik — kurs qoidasiga ko'ra (7 kun) o'rnatishda oldingisini oling.

bash
npx openapi-typescript ./openapi.yaml -o src/api/schema.d.ts

Natija — har endpoint uchun avtomatik turlar. Backend price ni satrga o'zgartirsa, faylni qayta generatsiya qilasiz va tsc frontend'dagi har bir price + 5000 ni qizil qiladi — xato build paytida, foydalanuvchidan oldin.

Diqqat: Generatsiya qilingan turlar ham as ga o'xshaydi — ular kontraktni aytadi, lekin ish vaqtida tekshirmaydi. Server hujjatga zid javob bersa (xato, eski versiya), turlar yordam bermaydi. Shuning uchun generatorlar sxema (Zod) ham yasashni taklif qiladi: tur — kompilyatsiya uchun, sxema — ish vaqti uchun. Bu vositalar kanonda o'lchanmagan; «Bahor» API'sida OpenAPI fayli yo'q.

5.2 To'liq TypeScript stekida — tRPC

Backend ham TypeScript bo'lsa va bitta repoda tursa, OpenAPI'siz ham bo'ladi: backend funksiyasining turi to'g'ridan frontend'ga o'tadi. Bu — tRPC g'oyasi (GraphQL va tRPC darsida tanishuv). vazifalar — oddiy REST, backend boshqa loyiha, shuning uchun bizning yo'l — qo'lda yozilgan Zod sxemasi.

6. Vazifalar qadami: Zod kontrakti (holat 12)

6.1 Nima o'zgaradi

bash
git switch -c feat/api-shartnoma
npm i --save-exact zod@4.6.5
  1. Yangi fayl api-sxema.ts — vazifa va ro'yxat sxemalari, xato matni.
  2. api.ts — shartnoma() va har metod tekshirilgan tur qaytaradi.
  3. paket.ts dan serverRoyxatiniTekshir va serverVazifasiniTekshir (15-qismdagi qo'lda yozilgan 50 qator) o'chiriladi, chaqiruvchilar soddalashadi.

6.2 api-sxema.ts

Kanondan aynan (izohlar telefon uchun qayta bo'lingan):

ts
// "zod/mini" — funksional API (.check(...)), klassik "zod" emas:
// o'lchandi (natijalar/12-zod-hajm.txt) — klassik +24.2 KB gzip,
// mini +5.8 KB; qoidalar va xabarlar bir xil
import { z } from "zod/mini";
import { Vazifa } from "./vazifa.ts";

// CSP (13/#32): script-src 'self' — eval yo'q. Zod 4 obyekt
// sxemalarini tezlashtirish uchun "new Function" bor-yo'qligini SINAB
// ko'radi; xato ushlansa ham brauzer buni CSP buzilishi deb e'lon
// qiladi (o'lchandi: natijalar/12-zod-csp.txt). jitless — sinovsiz
z.config({ jitless: true });

/** Bitta vazifa: faqat uch maydon, qolgani tashlanadi (oq ro'yxat) */
export const vazifaSxemasi = z.object({
  id: z.int().check(z.positive("id musbat son emas")),
  matn: z.string().check(
    z.refine((matn) => matn.trim() !== "", "matn bo'sh"),
    z.refine(
      (matn) => matn.length <= Vazifa.MAKS_UZUNLIK,
      "matn juda uzun",
    ),
  ),
  bajarildi: z.boolean(),
});

/** GET va PUT javobi: versiya — server hisobi (0 — kalit
 * yangi) */
export const serverRoyxatiSxemasi = z.object({
  versiya: z.int().check(z.nonnegative()),
  vazifalar: z
    .array(vazifaSxemasi)
    .check(
      z.refine(
        (vazifalar) =>
          new Set(vazifalar.map((v) => v.id)).size === vazifalar.length,
        "id takrorlangan",
      ),
    ),
});

export type ServerRoyxati = z.infer<typeof serverRoyxatiSxemasi>;
  • Vazifa.MAKS_UZUNLIK — 17-qismdagi o'sha qoida: sxema domen qoidasini qayta yozmaydi, undan oladi.
  • z.refine — maxsus shart: "matn bo'sh emas", "id takrorlanmaydi". Bu ikkinchisi — butun massiv ustidagi tekshiruv, bitta maydonda emas.
  • z.infer<typeof …> — tur sxemadan. ServerRoyxati ni qo'lda yozish shart emas; sxema o'zgarsa, tur ham o'zgaradi.
  • Xato matni — sxemaXatosi(): birinchi muammoning yo'li va xabari, «Bitta yordamchi» bo'limidagidek ("vazifalar.0.id: id musbat son emas").

6.3 api.ts — shartnoma va metodlar

ts
/**
 * Javob shartnomaga mos kelmasa — ApiXatosi (kod JAVOB): ekranga
 * "Saqlanmadi: ..." bo'lib chiqadi, Zod xatosi cause'da
 */
function shartnoma<T>(sxema: z.ZodMiniType<T>, malumot: unknown): T {
  const natija = sxema.safeParse(malumot);
  if (!natija.success) {
    throw new ApiXatosi(
      `Server javobi kutilgan shaklda emas (${sxemaXatosi(natija.error)}).`,
      { kod: "JAVOB", cause: natija.error },
    );
  }
  return natija.data;
}
  // … apiKlient ichida, har metod — so'rov, keyin shartnoma:
    royxat: async (sozlama?: SorovSozlamasi): Promise<ServerRoyxati> =>
      shartnoma(serverRoyxatiSxemasi, await yubor(manzil, sozlama)),
    // DELETE javobi bo'sh (204) — tekshiradigan narsa yo'q
    ochir: async (
      id: number,
      sozlama?: SorovSozlamasi,
    ): Promise<void> => {
      await yoz(`/${id}`, "DELETE", undefined, sozlama);
    },

shartnoma<T> — generic: sxemaning turidan natija turi chiqadi. Oldin metodlar Promise<unknown> qaytarardi va chaqiruvchi tekshirishi kerak edi — loader, action, server-sinxron.ts, App.tsx. Endi tekshiruv bitta joyda, chaqiruvchilarda bitta qator qisqardi (kanon diff):

diff
-    const javob = serverRoyxatiniTekshir(await api.royxat({ signal }));
+    const javob = await api.royxat({ signal });

6.4 Narxi: hajm, CSP va jitless

Hajm. Bir xil sxema uch variantda qurildi (natijalar/12-zod-hajm.txt va tajriba/hajm.txt):

Bir xil sxema: kutubxona hajmi (gzip)
  • zod 4.6.5 (klassik)ilovada24,2 KB
  • zod/mini 4.6.5 (kanon)5,8 KB
  • Valibot 1.5.0faqat sxema1,6 KB

Manba: O'lchandi: vite build, gzip -9, natijalar/12-zod-hajm.txt (holat 10: 122 503 B, klassik 146 721 B, mini 128 327 B) va natijalar/tajriba/hajm.txt (sxema-valibot 1 648 B), 2026-10-13

Klassik zod — zanjirli metodlar (z.string().min(1).max(100)), hamma narsa bitta obyektda, shuning uchun Vite foydalanilmagan qismlarni tashlab yubora olmaydi. zod/mini — funksiyalar (z.string().check(z.minLength(1))): ishlatilmagani to'plamga kirmaydi. Qoidalar va natija bir xil — 4 barobar kichik. Valibot yana ham kichik (1.6 KB) — u forma kutubxonalari muqobillari darsida; Zod — kurs yo'nalishi (15-qismdan beri).

CSP. vazifalar qattiq CSP ostida ishlaydi (script-src 'self', Trusted Types — Brauzer xavfsizlik modeli). Zod 4 obyekt sxemalarini tezlashtirish uchun new Function ishlay oladimi — sinab ko'radi. Kanon Chrome 155 da o'lchadi (natijalar/12-zod-csp.txt, aynan):

text
C:/Users/VICTUS/AppData/Local/Temp/kanon19/dist-zod-jit
   CSP buzilishlari: ["require-trusted-types-for trusted-types-sink index-tzfR33cv.js"]
   konsol xatolari: ["This document requires 'TrustedScript' assignment. The action has been blocked."]
../vazifalar-v9/dist
   CSP buzilishlari: []
   konsol xatolari: []

Birinchisi — jitless siz: Zod xatoni o'zi ushlab, sekinroq yo'lga o'tdi — ilova ishladi. Lekin brauzer CSP buzilishini qayd etdi va konsolda qizil xato chiqdi: "Bu hujjat 'TrustedScript' tayinlashni talab qiladi. Amal bloklandi." CSP hisobotlari yig'ilsa, har sahifa ochilishi — "hujum" xabari. Ikkinchisi — z.config({ jitless: true }) bilan: Zod sinamaydi, toza. Tezlik farqi bizning 100 ta vazifada sezilmaydi.

6.5 Tekshiruvlar

bash
git add -A
git commit

Commit (kanon): feat: API javoblari — Zod sxemasi (zod/mini, jitless), tiplangan metodlar.

  • npm test — node:test 182 (yangi api-sxema.test.js: to'g'ri javob — faqat uch maydon, buzuq id, takror, bo'sh matn; paket.test.js dagi server testlari shu yerga ko'chdi). Vitest 56.
  • Brauzer: 159/159, 72/72, 19-qism — 63/63. Yangilari (aynan):
text
✅ buzuq ro'yxat javobi: aloqa xato, ro'yxat — keshdan: ["Serverga ulanib bo'lmadi. Ro'yxat — shu brauzerdagi nusxa.",3]
✅ konsol: qaysi maydon buzuqligi aytiladi: true
✅ buzuq PATCH javobi: «Saqlanmadi», katak qaytdi: ["Saqlanmadi: Server javobi kutilgan shaklda emas (matn: matn bo'sh).",false]
✅ Zod CSP ostida: buzilish yo'q (jitless): []

Birinchisi — server buzuq ro'yxat yubordi: ilova yiqilmadi, brauzerdagi nusxani ko'rsatdi. Uchinchisi — PATCH javobida matn bo'sh: "Saqlanmadi", katakcha oldingi holatiga qaytdi (optimistik o'zgarish bekor). Hajm: gzip 122.5 → 128.3 KB (+5.8). Diff: 11 fayl, +250 −170.

Maslahat: Import fayli (paket.ts) hamon o'z qo'lda yozilgan tekshiruvida — u ham Zod'ga ko'chishi mumkin edi. Kanon buni ataylab qoldirdi (TEXNIK-QARZ): bitta qadamda bitta o'zgarish. Haqiqiy loyihalarda ham refaktor bo'laklarga bo'linadi.

7. Hujumchi nigohi

Server — ishonchli manba emas. Uning javobi ham foydalanuvchi kiritgan ma'lumot kabi tekshiriladi. Nega?

  • Server buzilgan bo'lishi mumkin. Hujumchi bazaga yozib qo'ydi: vazifa matni — <img src=x onerror=…> yoki 10 MB matn. React matnni escape qiladi, lekin uzunlik cheklovi (matn juda uzun) ekran va xotirani himoya qiladi.
  • Ortiqcha maydonlar — { "id": 1, "matn": "…", "admin": true, "__proto__": {…} }. z.object sxemada yo'q kalitlarni tashlaydi («Chegarada tekshirish» dagi uchinchi qator): admin ilova ichiga kirmaydi. Agar javobni { ...javob } bilan to'g'ridan holatga yoysangiz, hujumchi kutilmagan maydonlarni qo'shishi mumkin edi.
  • O'rtadagi odam (MITM) yoki eski kesh. Service Worker yoki proksi eski formatdagi javobni qaytardi. Sxema buni JAVOB xatosi bilan ushlaydi va ilova yiqilmaydi — vazifalar da keshdagi nusxaga qaytadi.
  • Teskari yo'nalish ham bor: frontend tekshiruvi serverni himoya qilmaydi. Server kelgan so'rovni o'zi tekshiradi — o'sha Zod sxemasi bilan, agar backend ham TypeScript bo'lsa (25-qism).

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
350005000 so'm — satr ulandi javob as bilan tiplangan, tekshirilmagan sxema bilan safeParse
fetch o'nta komponentda API qatlami yo'q api.ts — yagona eshik
500 javobi "muvaffaqiyat" res.ok tekshirilmagan qatlamda status → xato kodi
xatolar har xil turda normallashtirish yo'q bitta ApiXatosi, kod maydoni
"Server javobi noto'g'ri" — qaysi maydon? Zod xatosining yo'li tashlangan issues[0].path.join(".")
CSP: trusted-types-sink, konsolda TrustedScript Zod JIT sinovi z.config({ jitless: true })
to'plam +24 KB klassik zod zod/mini
tur va tekshiruv ajralib ketdi tur qo'lda, tekshiruv qo'lda z.infer<typeof sxema>

9. Mashqlar

Mashqlar kurs/mashqlar/19/12-api-shartnoma/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): qaysi kod?

vazifalar dagi ApiXatosi kodini yozing:

  1. Server 503 qaytardi →
  2. Javob keldi, lekin vazifalar maydoni massiv emas →
  3. Foydalanuvchi sahifadan ketdi, router request.signal ni bekor qildi →
  4. Wi-Fi o'chgan →
Yechim

SERVER (5xx), JAVOB (shartnoma buzildi — status 200 bo'lsa ham), BEKOR (xato emas, UI hech narsa ko'rsatmaydi), TARMOQ (fetch TypeError tashladi). Ikkinchisiga e'tibor bering: HTTP nuqtai nazaridan hammasi yaxshi (200), lekin kontrakt bo'yicha — xato. Sxema bo'lmaganda bu holat umuman xato bo'lmasdi — komponent undefined.map da yiqilardi.

2-mashq (o'rta): buyurtma sxemasi

«Bahor» buyurtma javobi: { id, items: [{ name, qty }], total }. Sxema yozing: id — musbat butun son, items — kamida bitta element, qty — musbat butun son, total — manfiy bo'lmagan son. Xabarlar o'zbekcha. Quyidagi uchta javobni tekshirib, har biriga ok yoki yo'l — xabar chiqaring.

Ishora: «Chegarada tekshirish» bo'limi; massiv uzunligi — z.minLength(1, "…").

jsx
import { z } from "zod/mini";

z.config({ jitless: true });

const responses = [
  { id: 7, items: [{ name: "Osh", qty: 2 }], total: 70000 },
  { id: 8, items: [], total: 0 },
  { id: 9, items: [{ name: "Manti", qty: 0 }], total: 30000 },
];

// orderSchema va tekshiruv — shu yerga
Yechim
jsx
import { z } from "zod/mini";

z.config({ jitless: true });

const orderSchema = z.object({
  id: z.int().check(z.positive("id musbat emas")),
  items: z
    .array(
      z.object({
        name: z.string(),
        qty: z.int().check(z.positive("soni musbat emas")),
      }),
    )
    .check(z.minLength(1, "buyurtma bo'sh")),
  total: z.number().check(z.nonnegative("jami manfiy")),
});

const responses = [
  { id: 7, items: [{ name: "Osh", qty: 2 }], total: 70000 },
  { id: 8, items: [], total: 0 },
  { id: 9, items: [{ name: "Manti", qty: 0 }], total: 30000 },
];

for (const r of responses) {
  const result = orderSchema.safeParse(r);
  if (result.success) {
    console.log(r.id, "ok");
  } else {
    const [issue] = result.error.issues;
    console.log(r.id, issue.path.join("."), "—", issue.message);
  }
}

export default function App() {
  return <p>Natija konsolda</p>;
}

Konsolda:

text
7 ok
8 items — buyurtma bo'sh
9 items.0.qty — soni musbat emas

8-buyurtmada xato butun massivda (items), 9-da — ichma-ich yo'l (items.0.qty): massivning birinchi elementi, qty maydoni. Yo'l qanchalik chuqur bo'lmasin, Zod uni to'liq beradi.

3-mashq (qiyin): API qatlami bilan sahifa

api.order(id) metodini yozing: soxta server (fakeServer(id), tayyor) javobini contract() bilan tekshirsin. Komponent ikki buyurtmani ko'rsatsin: 7 (to'g'ri) va 9 (buzuq). Buzuq bo'lsa — Buyurtma #9: ochib bo'lmadi, konsolda JAVOB va xabar. Ishora: «Bitta yordamchi» bo'limidagi contract va ApiError; 2-mashqdagi sxemaning qisqa varianti tayyor.

Yechim
jsx
import { useEffect, useState } from "react";
import { z } from "zod/mini";

z.config({ jitless: true });

class ApiError extends Error {
  constructor(message, { code, cause }) {
    super(message, { cause });
    this.name = "ApiError";
    this.code = code;
  }
}

const orderSchema = z.object({
  id: z.int(),
  total: z.number("jami son emas"),
});

function contract(schema, data) {
  const result = schema.safeParse(data);
  if (!result.success) {
    const [issue] = result.error.issues;
    throw new ApiError(
      `Javob noto'g'ri (${issue.path.join(".")}: ${issue.message})`,
      { code: "JAVOB", cause: result.error },
    );
  }
  return result.data;
}

const DATA = { 7: { id: 7, total: 70000 }, 9: { id: 9, total: "?" } };
const fakeServer = (id) =>
  new Promise((resolve) => setTimeout(() => resolve(DATA[id]), 200));

const api = {
  order: async (id) => contract(orderSchema, await fakeServer(id)),
};

function Order({ id }) {
  const [text, setText] = useState("…");
  useEffect(() => {
    api
      .order(id)
      .then((o) => setText(`${o.total} so'm`))
      .catch((error) => {
        console.warn(error.code, error.message);
        setText("ochib bo'lmadi");
      });
  }, [id]);
  return (
    <p>
      Buyurtma #{id}: {text}
    </p>
  );
}

export default function App() {
  return (
    <>
      <Order id={7} />
      <Order id={9} />
    </>
  );
}

Konsolda:

text
JAVOB Javob noto'g'ri (total: jami son emas)

Ekranda:

text
Buyurtma #7: 70000 so'm
Buyurtma #9: ochib bo'lmadi

Bitta sahifada bir buyurtma ishladi, ikkinchisi xato — ilova yiqilmadi, faqat bitta qator "ochib bo'lmadi". Order sxemani ham, fetch ni ham bilmaydi: faqat api.order(id). Keyingi darslarda TanStack Query aynan shunday api metodini queryFn sifatida oladi.

4-mashq: Vazifalar qadami

feat/api-shartnoma branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling: zod@4.6.5, api-sxema.ts, api.ts dagi shartnoma va metod turlari, paket.ts dan server tekshiruvlarini olib tashlash, testlar. Keyin commit.

Yechim

Tekshiruv ro'yxati:

  • npm run tip — toza; api.royxat() ning turi Promise<ServerRoyxati> (VS Code'da sichqonchani ustiga olib boring).
  • grep -r "serverRoyxatiniTekshir" src — bo'sh.
  • DevTools → Network → javobni "Override" qilib, bitta vazifaning matn ini "" qiling: ro'yxat "Serverga ulanib bo'lmadi…", konsolda vazifalar.N.matn: matn bo'sh.
  • DevTools → Console — CSP yoki TrustedScript xatosi yo'q.
  • npm test — node 182, Vitest 56; build — gzip ~128 KB.

10. Real ishda

  • Har jiddiy frontend — API qatlami (api/, services/) va xato normallashtirish. Intervyuda "API xatolarini qanday boshqarasiz?" — klassik savol.
  • Zod chegarada — Next.js, Remix, tRPC loyihalarida server javobi va forma ma'lumoti Zod bilan tekshiriladi. "Parse, don't validate" — tekshirilgan qiymatni turi bilan qaytarish g'oyasi.
  • OpenAPI kodgen — katta jamoalarda (backend alohida) orval yoki Hey API bilan klient va hook'lar avtomatik yasaladi; CI'da "OpenAPI o'zgardi — frontend build yiqildi" — normal holat.
  • Xato kuzatish — cause dagi Zod xatosi Sentry'ga ketadi: "1.price — narx son emas" degan xabar backend jamoasiga darhol yo'l ko'rsatadi.

Xulosa

  • API qatlami — frontend va backend orasidagi yagona eshik: manzil, vaqt chegarasi, bekor qilish, xatolar va tekshiruv bitta joyda.
  • TypeScript turi (as) — va'da; server javobi ish vaqtida sxema bilan tekshiriladi (safeParse).
  • shartnoma(sxema, javob) — mos kelsa tiplangan qiymat, aks holda ApiXatosi (JAVOB) yo'l bilan: "vazifalar.0.id: …".
  • Xatolar bitta turga normallashtiriladi: TARMOQ, VAQT, BEKOR, TOPILMADI, SERVER, JAVOB.
  • z.object ortiqcha maydonlarni tashlaydi (oq ro'yxat); z.infer — tur sxemadan.
  • Kanon: zod/mini (+5.8 KB, klassik +24.2), jitless (CSP toza). OpenAPI bo'lsa — openapi-typescript, orval, Hey API.

Keyingi dars: TanStack Query I: useQuery asoslari — vazifalar ning server state'i bitta keshga ko'chadi: useQuery, queryKey, queryFn va yuklanish holatlari.

Manbalar

  • Zod 4: Basics, Zod Mini, safeParse, z.config({ jitless }) — zod.dev (v4.6.5)
  • OpenAPI Specification — spec.openapis.org; openapi-typescript — openapi-ts.dev; orval — orval.dev; Hey API — heyapi.dev
  • MDN: Content Security Policy, Trusted Types, Error: cause — developer.mozilla.org
  • Alexis King: "Parse, don't validate" (2019) — lexi-lambda.github.io
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
API qatlami va type-safe kontrakt: Zod bilan server javobini tekshirish, xatolarni normallashtirish va OpenAPI — IlmHamroh