IlmHamroh
JavaScript Full-stack/11-qism. Asinxron JavaScript va tarmoq22/32-dars26 daqiqa
Mundarija (32)

API klient qatlami: hamma fetch larni bitta joyga yig'ish

Qisqacha: API klient qatlami — ilovadagi hamma so'rovlar o'tadigan bitta modul (api.js). U asosiy URL, sarlavhalar, javob.ok tekshiruvi va timeout'ni bir joyda bajaradi. Har xil xatolarni — tarmoq, timeout, 404, 500, buzuq JSON — bitta ApiXatosi shakliga (holat, kod) keltiradi. Interfeys esa fetch ni umuman ko'rmaydi: await api.qosh("Non olish") deydi, xolos.

Bu darsda

  • Tarqoq fetch lar nega xavfli ekanini va o'ram (wrapper) qanday yordam berishini tushuntira olasiz.
  • sorovYubor funksiyasini yozasiz: asosiy URL, JSON, javob.ok, timeout.
  • Besh xil muvaffaqiyatsizlikni bitta ApiXatosi (holat, kod) ga keltirasiz.
  • Interfeysda "yuklanmoqda / xato / ma'lumot" holatlarini to'g'ri ko'rsatasiz.
  • Eski javob yangisining ustidan yozib yuboradigan race condition dan himoyalanasiz.
  • vazifalar uchun api.js ni yaratasiz.

Oldin bilishingiz kerak: fetch asoslari, fetch bilan ma'lumot yuborish, CORS mijoz tomondan, Asinxron naqshlar, Bekor qilish: AbortController, O'z xato klasslaringiz.

1. Nega bu kerak?

«Bahor»da mehmon oshxonaga o'zi kirmaydi. U ofitsiant Otabekka aytadi: "Bitta osh". Otabek esa hamma narsani biladi: buyurtma qaysi oynaga beriladi, osh tugagan bo'lsa nima deyiladi, oshxona javob bermasa qancha kutiladi. Mehmon uchun bu tafsilotlar yo'q — u faqat "osh keldi" yoki "osh tugadi, lag'mon bor" eshitadi.

Endi vazifalar ilovasini serverga ulayotganimizni tasavvur qiling. Vazifa qo'shish, belgilash, o'chirish, ro'yxatni olish — to'rtta joyda fetch. Har birida bir xil ishlar takrorlanadi:

js
// ❌ Har tinglovchida o'z fetch'i
const javob = await fetch(
  "https://ilmhamroh.uz/api/mashq/vazifalar/" + kalit,
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ matn }),
  },
);
if (!javob.ok) {
  elonQil("Xato!");
  return;
}
const vazifa = await javob.json();

Bir oydan keyin nima bo'ladi?

  • Bitta joyda javob.ok tekshiruvi unutilgan. Server 500 qaytaradi, ilova esa xato matnini "vazifa" deb chizadi.
  • Server manzili o'zgardi — 12 ta faylni qidirib, 12 joyni tuzatasiz. Bittasi qolib ketadi.
  • Bir joyda xato "Xato!", boshqasida "Error", uchinchisida umuman jim. Foydalanuvchi nima bo'lganini tushunmaydi.
  • Timeout hech qayerda yo'q. Sekin internetda tugma abadiy "kutilmoqda".

Yechim — API klient qatlami (API client layer): server bilan gaplashadigan yagona modul. Ilovaning qolgan qismi uchun u Otabek: api.qosh("Non olish") deydi va yo vazifa oladi, yo tushunarli xato. fetch, URL, sarlavha, timeout — hammasi shu modul ichida.

flowchart TD
  UI["Interfeys<br/>render.js"] -->|"api.qosh()"| K["API klient<br/>api.js"]
  K --> S["sorovYubor<br/>URL, JSON, timeout"]
  S -->|fetch| SV[("Server<br/>/api/mashq")]
  SV -->|javob| S
  S -->|"ma'lumot yoki ApiXatosi"| K
  K --> UI

Sxemaga qarang: interfeys faqat yuqoridagi strelkani biladi. fetch — faqat pastda, bitta joyda.

2. Birinchi o'ram: sorovYubor

2.1 Asosiy URL va javob.ok

O'ram (wrapper) — boshqa funksiyani ichida chaqirib, unga qo'shimcha ish qo'shadigan funksiya. Bizniki fetch ni o'raydi.

Birinchi qadam: takrorlanadigan uch narsani bitta joyga olamiz — asosiy URL, JSON o'qish va javob.ok tekshiruvi.

js
const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";

async function sorovYubor(yol) {
  const javob = await fetch(ASOSIY_URL + yol);
  const malumot = await javob.json();
  if (!javob.ok) {
    throw new Error(malumot.xato);
  }
  return malumot;
}

const osh = await sorovYubor("/menyu/1");
console.log(osh.nom, osh.narx); // Osh 35000

try {
  await sorovYubor("/menyu/99");
} catch (xato) {
  console.log("Xato:", xato.message); // Xato: Taom topilmadi
}

Asosiy URL (base URL) — hamma so'rovlar uchun umumiy boshlanish: https://ilmhamroh.uz/api/mashq. Har chaqiruv faqat yo'l (path) beradi: /menyu/1. Server boshqa manzilga ko'chsa — bitta qatorni o'zgartirasiz.

Endi sorovYubor 404 da xato tashlaydi — fetch ning o'zi buni qilmasdi (fetch asoslari). Chaqiruvchi try/catch bilan ushlaydi. Muvaffaqiyatli javob esa to'g'ridan-to'g'ri ma'lumot: osh.nom.

2.2 Ma'lumot yuborish

Yozish so'rovlari (fetch bilan ma'lumot yuborish) uchun metod va tana kerak. JSON.stringify va Content-Type ni ham o'ram o'z zimmasiga oladi:

js
const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";

async function sorovYubor(yol, { metod = "GET", tana } = {}) {
  const sozlama = { method: metod, headers: {} };
  if (tana !== undefined) {
    sozlama.headers["Content-Type"] = "application/json";
    sozlama.body = JSON.stringify(tana);
  }
  const javob = await fetch(ASOSIY_URL + yol, sozlama);
  const malumot = await javob.json();
  if (!javob.ok) {
    throw new Error(malumot.xato);
  }
  return malumot;
}

const aks = await sorovYubor("/aks", {
  metod: "POST",
  tana: { taom: "Manti", soni: 2 },
});
console.log(aks.metod, aks.sarlavhalar["content-type"]);
console.log(aks.tana);

Konsolda:

text
POST application/json
{ taom: 'Manti', soni: 2 }

/aks — mashq API'ning "oynasi": u kelgan so'rovni qaytaradi. Ko'ryapsizmi, metod, sarlavha va tana to'g'ri ketdi.

Ikkinchi parametr — sozlama obyekti (Parametrda destructuring va options obyekti). = {} — sozlama berilmasa ham destructuring xato bermasin. metod = "GET" — standart qiymat.

Tekshirib ko'ring: sorovYubor("/menyu") chaqirilganda sozlama.body bormi? Content-Type chi?

Javob

Ikkalasi ham yo'q. tana berilmadi, ya'ni undefined — if ichi bajarilmaydi. GET so'rovning tanasi bo'lmaydi, shuning uchun Content-Type ham kerak emas.

3. Xatolarni bitta shaklga keltirish

3.1 Besh xil muvaffaqiyatsizlik

Hozirgi sorovYubor faqat bitta holatni yaxshi biladi: server JSON xato qaytardi. Real hayotda esa so'rov besh xil yo'l bilan muvaffaqiyatsiz bo'ladi:

Nima bo'ldi JavaScript'da nima keladi Bizning kod
Internet yo'q, server topilmadi TypeError: fetch failed (Chrome: Failed to fetch) TARMOQ
Server juda uzoq javob bermadi TimeoutError VAQT
So'rovni o'zimiz bekor qildik AbortError BEKOR
Server 4xx/5xx qaytardi javob.ok === false TOPILMADI, NOTOGRI, SERVER, HTTP
Javob JSON emas (HTML xato sahifasi) SyntaxError JAVOB

Har birining shakli har xil: biri TypeError, biri DOMException (TimeoutError va AbortError shu turdagi xatolar), biri umuman xato emas — oddiy javob. Interfeys bularni alohida-alohida tanishi kerakmi? Yo'q. Xato normalizatsiyasi (normalization) — har xil xatolarni bitta shaklga keltirish. Bizda u shakl — ApiXatosi:

  • holat — HTTP holat kodi (404, 500); server javob bermagan bo'lsa 0.
  • kod — qisqa, dasturchi uchun nom: TARMOQ, VAQT, TOPILMADI ...
  • message — foydalanuvchiga ko'rsatsa bo'ladigan o'zbekcha matn.

Bu O'z xato klasslaringiz darsidagi g'oya: vazifalar dagi VazifaXatosi ham kod bilan edi.

3.2 ApiXatosi va to'liq sorovYubor

Endi besh holatni ham ushlaydigan versiya. Kod uzun, shuning uchun avval uning yo'lini sxemada ko'raylik. Javob yuqoridan pastga uchta "elak" dan o'tadi. Qaysi elakda to'xtasa — o'sha kod chiqadi:

flowchart TD
  F["fetch + text()"] -->|"xato tashladi"| T["tarmoqXatosi:<br/>VAQT / BEKOR / TARMOQ"]
  F -->|"matn keldi"| J["JSON.parse"]
  J -->|"buzuq JSON"| JA["JAVOB"]
  J -->|"o'qildi"| O{"javob.ok?"}
  O -->|"yo'q"| H["holatKodi:<br/>TOPILMADI / NOTOGRI /<br/>SERVER / HTTP"]
  O -->|"ha"| M["ma'lumot qaytadi"]

Endi kodning o'zi. Pastdagi besh sinov har bir holatni ko'rsatadi:

js
const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";

class ApiXatosi extends Error {
  constructor(xabar, sozlama) {
    super(xabar, sozlama);
    this.name = "ApiXatosi";
    this.holat = sozlama?.holat ?? 0;
    this.kod = sozlama?.kod ?? "NOMALUM";
  }
}

function holatKodi(holat) {
  if (holat === 404) return "TOPILMADI";
  if (holat === 400 || holat === 422) return "NOTOGRI";
  if (holat >= 500) return "SERVER";
  return "HTTP";
}

function tarmoqXatosi(xato) {
  if (xato.name === "TimeoutError") {
    return new ApiXatosi("Server javob bermadi.", {
      kod: "VAQT", cause: xato,
    });
  }
  if (xato.name === "AbortError") {
    return new ApiXatosi("So'rov bekor qilindi.", {
      kod: "BEKOR", cause: xato,
    });
  }
  return new ApiXatosi("Internet aloqasini tekshiring.", {
    kod: "TARMOQ", cause: xato,
  });
}

async function sorovYubor(yol, { kutish = 8000 } = {}) {
  let javob;
  let matn;
  try {
    javob = await fetch(ASOSIY_URL + yol, {
      signal: AbortSignal.timeout(kutish),
    });
    matn = await javob.text();
  } catch (xato) {
    throw tarmoqXatosi(xato);
  }
  let malumot = null;
  if (matn !== "") {
    try {
      malumot = JSON.parse(matn);
    } catch (xato) {
      throw new ApiXatosi("Server JSON qaytarmadi.", {
        holat: javob.status, kod: "JAVOB", cause: xato,
      });
    }
  }
  if (!javob.ok) {
    const xabar = malumot?.xato ?? `Server xatosi: ${javob.status}`;
    throw new ApiXatosi(xabar, {
      holat: javob.status, kod: holatKodi(javob.status),
    });
  }
  return malumot;
}

const sinovlar = [
  ["/menyu/1", {}],
  ["/menyu/99", {}],
  ["/holat/503", {}],
  ["/oqim?bolaklar=1", {}],
  ["/menyu?kechikish=3000", { kutish: 1000 }],
];
for (const [yol, sozlama] of sinovlar) {
  try {
    const taom = await sorovYubor(yol, sozlama);
    console.log("✅", taom.nom);
  } catch (xato) {
    console.log(`❌ ${xato.kod} (${xato.holat}): ${xato.message}`);
  }
}

Konsolda:

text
✅ Osh
❌ TOPILMADI (404): Taom topilmadi
❌ SERVER (503): Server xatosi: 503
❌ JAVOB (200): Server JSON qaytarmadi.
❌ VAQT (0): Server javob bermadi.

Qismma-qism:

  • try ichida fetch va text(). Ikkalasi ham tarmoq bilan ishlaydi: ulanish uzilishi yoki timeout ikkalasining o'rtasida ham bo'lishi mumkin. catch ularni tarmoqXatosi ga beradi — u name ga qarab VAQT, BEKOR yoki TARMOQ yasaydi.
  • json() emas, text() + JSON.parse. Shunda "javob keldi, lekin JSON emas" holatini alohida ushlaymiz. /oqim oddiy matn qaytaradi — sinovda shu JAVOB bo'ldi. Real hayotda bu ko'pincha HTML xato sahifasi bo'ladi. Masalan, nginx ilova ishlamay qolsa "502 Bad Gateway" degan HTML qaytaradi. Nginx — server oldida turib, so'rovlarni ilovaga uzatadigan dastur; uni backend qismlarida o'rnatamiz.
  • Bo'sh tana. 204 No Content javobida tana yo'q: matn === "". Unda malumot — null, xato emas.
  • malumot?.xato ?? ... — server xabari bo'lsa, o'shani olamiz. /holat/503 ning tanasida xato maydoni yo'q (u {holat, xabar} qaytaradi). Shunda zaxira matn ishladi: Server xatosi: 503.
  • cause — asl xato yo'qolmaydi. Konsolda faqat bizning matn, lekin xatoni tekshirayotgan dasturchi xato.cause dan asl TypeError ni ko'radi.

Endi interfeys kodi faqat bitta narsani biladi: catch (xato) ichida xato.kod va xato.message bor. Qaysi kodda nima qilishni o'zi hal qiladi: TARMOQ — "Internetni tekshiring" va «Qayta urinish» tugmasi, NOTOGRI — maydon yonida xabar, BEKOR — umuman hech narsa ko'rsatmaslik.

Tekshirib ko'ring: Server 401 qaytarsa, holatKodi qanday kod beradi? xato.holat chi?

Javob

kod — HTTP (401 ro'yxatdagi uchta shartga ham tushmaydi), holat — 401. Ilovaga kirish (login) kerak bo'lganda 401 ni alohida kod bilan ajratish foydali — buni Autentifikatsiyali so'rovlar darsida qilamiz.

4. Timeout va bekor qilish birga

fetch ning o'zida timeout yo'q. Server javob bermasa, brauzer bir necha daqiqa kutishi mumkin. Shuning uchun har so'rovga AbortSignal.timeout(ms) beramiz — Bekor qilish: AbortController darsidan tanish.

Lekin ba'zan chaqiruvchi ham o'z signalini bermoqchi: "foydalanuvchi sahifadan chiqib ketdi — so'rovni to'xtat". fetch esa faqat bitta signal oladi. Ikki signalni birlashtirish — AbortSignal.any([a, b]): ulardan qaysi biri birinchi to'xtasa, natija ham to'xtaydi.

js
const vaqt = AbortSignal.timeout(kutish);
const signal = tashqiSignal
  ? AbortSignal.any([tashqiSignal, vaqt])
  : vaqt;

Shu bilan bitta so'rov ikki sababdan to'xtashi mumkin. tarmoqXatosi ularni farqlaydi: timeout — TimeoutError → VAQT, qo'lda abort() — AbortError → BEKOR. Bu farq muhim: timeout — muammo, foydalanuvchiga aytish kerak. Bekor qilish esa ataylab qilingan — xato xabari kerak emas.

kutish qancha bo'lsin? Ko'p loyihalarda 8–15 soniya. Juda qisqa bo'lsa, sekin mobil internetda (Aziz kabi foydalanuvchilarda) hamma so'rov VAQT bo'ladi. Juda uzun bo'lsa — foydalanuvchi tugma abadiy aylanayotganini ko'radi.

5. Yuklanmoqda, xato, ma'lumot

5.1 Uch holat

Har so'rovning interfeysdagi hayoti bir xil:

stateDiagram-v2
  [*] --> Bosh
  Bosh --> Yuklanmoqda: so'rov
  Yuklanmoqda --> Tayyor: ma'lumot keldi
  Yuklanmoqda --> Xato: ApiXatosi
  Xato --> Yuklanmoqda: Qayta urinish
  Tayyor --> Yuklanmoqda: yangilash

Uch holatni bitta obyektda saqlaymiz va ekranni faqat render() chizadi — Holatdan UI chizish: state → render darsidagi qoida:

  • yuklanmoqda: true — tugma o'chiriladi (ikki marta bosilmasin), "Yuklanmoqda..." ko'rsatiladi.
  • xato — xabar va «Qayta urinish» tugmasi.
  • taomlar — ro'yxat.
html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  button { font: inherit; margin: 0 0.25rem 0.5rem 0; }
  .xato { color: #b3261e; }
</style>
<button id="menyu" type="button">Menyu</button>
<button id="yoq" type="button">Yo'q taom (404)</button>
<p id="holat" role="status"></p>
<ul id="royxat"></ul>
<script>
  const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";
  const holat = { yuklanmoqda: false, xato: null, taomlar: [] };
  const holatEl = document.getElementById("holat");
  const royxatEl = document.getElementById("royxat");
  const tugmalar = document.querySelectorAll("button");

  function render() {
    for (const tugma of tugmalar) tugma.disabled = holat.yuklanmoqda;
    holatEl.className = holat.xato ? "xato" : "";
    if (holat.yuklanmoqda) holatEl.textContent = "Yuklanmoqda...";
    else if (holat.xato) holatEl.textContent = holat.xato;
    else holatEl.textContent = `${holat.taomlar.length} ta taom`;
    royxatEl.replaceChildren(...holat.taomlar.map((t) => {
      const li = document.createElement("li");
      li.textContent = `${t.nom} — ${t.narx} so'm`;
      return li;
    }));
  }

  async function yukla(yol) {
    holat.yuklanmoqda = true;
    holat.xato = null;
    render();
    console.log("holat: yuklanmoqda");
    try {
      const javob = await fetch(ASOSIY_URL + yol);
      const malumot = await javob.json();
      if (!javob.ok) throw new Error(malumot.xato);
      holat.taomlar = Array.isArray(malumot) ? malumot : [malumot];
      console.log("holat: tayyor,", holat.taomlar.length, "ta");
    } catch (xato) {
      holat.xato = `Xato: ${xato.message}`;
      holat.taomlar = [];
      console.log("holat: xato —", xato.message);
    } finally {
      holat.yuklanmoqda = false;
      render();
    }
  }

  document.getElementById("menyu").addEventListener("click", () => {
    yukla("/menyu?kechikish=1200");
  });
  document.getElementById("yoq").addEventListener("click", () => {
    yukla("/menyu/99?kechikish=800");
  });
  yukla("/menyu?kechikish=1200");
</script>

Konsolda:

text
holat: yuklanmoqda
holat: tayyor, 4 ta

Sahifa ochilganda 1,2 soniya "Yuklanmoqda..." va o'chirilgan tugmalar ko'rinadi. ?kechikish=1200 — server ataylab kechikadi, sekin internetni sinash uchun. Keyin «Yo'q taom (404)» ni bosing: xabar qizil bo'ladi, ro'yxat tozalanadi.

finally muhim: muvaffaqiyat ham, xato ham bo'lsa — yuklanmoqda albatta false bo'ladi. Aks holda xatodan keyin tugmalar abadiy o'chiq qolardi.

role="status" — ekran o'quvchi holat o'zgarishini o'qib beradi. Server xabari esa textContent bilan chiqdi — innerHTML emas (sababini «Hujumchi nigohi» da ko'ramiz).

5.2 "Yuklanmoqda" qachon ko'rinsin

Kichik, lekin foydali tafsilot: javob 100 ms da kelsa, "Yuklanmoqda..." bir lahzaga miltillaydi va ko'zni charchatadi. Ko'p ilovalar "spinner" (aylanib turadigan kutish belgisi) ni faqat so'rov 300–500 ms dan uzoq cho'zilsa ko'rsatadi: setTimeout bilan kechiktiriladi va javob kelganda clearTimeout qilinadi (Taymerlar). Bu — 2-mashqning bir qismi bo'lishi mumkin.

6. Race condition: eski javob yangisini bosib ketadi

6.1 Muammo

«Bahor» saytida turkum tugmalari bor: «Taomlar», «Ichimliklar». Mehmon «Taomlar» ni bosdi, keyin fikrini o'zgartirib, darhol «Ichimliklar» ni bosdi. Ekranda ichimliklar bo'lishi kerak.

Lekin internet tekis ishlamaydi: birinchi so'rov sekin keldi (800 ms), ikkinchisi tez (100 ms). Buni ?kechikish= bilan yasaymiz. Ikki tez bosishni esa Promise.all bilan taqlid qilamiz: ikkala chaqiruv deyarli bir vaqtda boshlanadi, xuddi tugmalar ketma-ket bosilgandek. ekranda o'zgaruvchisi — ekranning o'rnida:

js
const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";
let ekranda = "";

async function turkumniKorsat(turkum, kechikish) {
  const url =
    `${ASOSIY_URL}/menyu?turkum=${turkum}&kechikish=${kechikish}`;
  const javob = await fetch(url);
  const taomlar = await javob.json();
  ekranda = `${turkum}: ${taomlar.map((t) => t.nom).join(", ")}`;
}

await Promise.all([
  turkumniKorsat("taom", 800),
  turkumniKorsat("ichimlik", 100),
]);
console.log(ekranda); // taom: Osh, Lag'mon, Manti

Mehmon oxirida «Ichimliklar» ni tanlagan edi, ekranda esa taomlar. Ichimliklar javobi 100 ms da keldi va chizildi, keyin 800 ms da eski javob keldi va uning ustidan yozdi.

Race condition (poyga holati) — natija ishlarning qaysi tartibda tugashiga bog'liq bo'lib qoladigan xato. Bu xato faqat ba'zan chiqadi — sekin tarmoqda — va shuning uchun xavfli: dasturchi kompyuterida hech qachon ko'rinmaydi. Asinxron naqshlar darsida "oxirgi so'rov yutadi" naqshini ko'rgan edik — endi uni haqiqiy fetch ga qo'llaymiz.

Ikki yechim bor.

6.2 Yechim 1: eskisini bekor qilish

Har yangi so'rov eskisining AbortController ini abort() qiladi:

js
const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";
let ekranda = "";
let joriy = null;

async function turkumniKorsat(turkum, kechikish) {
  joriy?.abort();
  const boshqaruvchi = new AbortController();
  joriy = boshqaruvchi;
  const url =
    `${ASOSIY_URL}/menyu?turkum=${turkum}&kechikish=${kechikish}`;
  try {
    const javob = await fetch(url, { signal: boshqaruvchi.signal });
    const taomlar = await javob.json();
    ekranda = `${turkum}: ${taomlar.map((t) => t.nom).join(", ")}`;
  } catch (xato) {
    if (xato.name === "AbortError") {
      console.log(`«${turkum}» bekor qilindi`);
      return;
    }
    throw xato;
  }
}

await Promise.all([
  turkumniKorsat("taom", 800),
  turkumniKorsat("ichimlik", 100),
]);
console.log(ekranda);

Konsolda:

text
«taom» bekor qilindi
ichimlik: Ko'k choy

joriy?.abort() — oldingi so'rov bo'lsa, uni to'xtatadi. ?. — birinchi chaqiruvda joriy hali null. Bekor qilingan so'rov AbortError bilan tugaydi — uni xato deb ko'rsatmaymiz, jim chiqamiz. Bonus: brauzer eski ulanishni uzadi, trafik tejaladi.

6.3 Yechim 2: so'rov raqami

Ba'zan so'rovni bekor qilib bo'lmaydi (masalan, u boshqa kutubxona ichida). Unda har so'rovga raqam beramiz va javob kelganda tekshiramiz: "men hali oxirgisimanmi?"

js
const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";
let ekranda = "";
let oxirgiRaqam = 0;

async function turkumniKorsat(turkum, kechikish) {
  const raqam = ++oxirgiRaqam;
  const url =
    `${ASOSIY_URL}/menyu?turkum=${turkum}&kechikish=${kechikish}`;
  const javob = await fetch(url);
  const taomlar = await javob.json();
  if (raqam !== oxirgiRaqam) {
    console.log(`«${turkum}» eskirdi — tashlandi`);
    return;
  }
  ekranda = `${turkum}: ${taomlar.map((t) => t.nom).join(", ")}`;
}

await Promise.all([
  turkumniKorsat("taom", 800),
  turkumniKorsat("ichimlik", 100),
]);
console.log(ekranda);

Konsolda:

text
«taom» eskirdi — tashlandi
ichimlik: Ko'k choy

++oxirgiRaqam — avval oshiradi, keyin qiymatni beradi: birinchi so'rov 1, ikkinchisi 2. Taom javobi kelganda oxirgiRaqam allaqachon 2 — demak u eskirgan. Bu yechimda so'rov oxirigacha boradi, faqat natijasi tashlanadi.

Tekshirib ko'ring: «Muammo» bo'limidagi kodda kechikishlarni almashtirsak — taom 100 ms, ichimlik 800 ms — xato ko'rinadimi?

Javob

Yo'q: eski javob (taom) birinchi keladi, yangisi (ichimlik) keyin keladi va to'g'ri yozadi. Race condition shuning uchun xavfli — u faqat ma'lum tezliklarda ko'rinadi. Himoya har doim bo'lishi kerak, "bizda ishlayapti" yetarli emas.

7. Klient obyekti: apiKlient

sorovYubor umumiy ishni qiladi. Ilovaning qolgan qismi uchun esa yanada qulayroq — resurs metodlari. Resurs — serverdagi bitta "narsa" turi (bizda — vazifalar ro'yxati). Resurs metodi shu narsa bilan bitta ishni bajaradi: api.royxat() — ro'yxatni olish, api.qosh(matn) — vazifa qo'shish. Ularni Closure amaliyotda dagi fabrika bilan yasaymiz:

js
export function apiKlient({ asosiyUrl, kalit }) {
  const manzil = `${asosiyUrl}/vazifalar/${kalit}`;
  return {
    royxat: (sozlama) => sorovYubor(manzil, sozlama),
    qosh: (matn, sozlama) => sorovYubor(manzil, {
      ...sozlama, metod: "POST", tana: { matn },
    }),
    // yangila, ochir ...
  };
}

Fabrikaning foydasi: asosiyUrl va kalit bir marta beriladi va closure'da eslab qolinadi. Sinov uchun boshqa server, haqiqiy ilova uchun boshqasi — ikkita klient, kod bir xil.

Har metod oxirgi parametr sifatida sozlama oladi va uni ...sozlama bilan sorovYubor ga uzatadi. Shunda chaqiruvchi signal yoki kutish bera oladi: api.royxat({ signal }).

8. Hujumchi nigohi

API qatlami — brauzer va server orasidagi chegara. Bu yerda uch narsa buziladi.

Server matnini HTML qilib chiqarish. holatEl.innerHTML = xato.message — xavfli. Hujumchi so'rov yo'lidagi biror bo'g'inni boshqarsa — o'z serverini yoki o'rtadagi proksini (brauzer va server orasida so'rovni uzatib turuvchi kompyuter) — xato maydoniga <img src=x onerror="..."> qo'yadi. Natijada foydalanuvchi brauzerida o'z kodini ishga tushiradi (XSS — Kontentni o'zgartirish va XSS xavfi). Himoya: server matni — faqat textContent, bizning natijadagi kabi.

Maxfiy kalitni frontend'ga yozish. Ba'zi API'lar "API key" talab qiladi: ob-havo, SMS, to'lov. const KALIT = "sk_live_..." ni api.js ga yozsangiz, uni har bir foydalanuvchi DevTools'ning Sources panelida o'qiydi va sizning hisobingizdan foydalanadi. Himoya: maxfiy kalit faqat serverda. Brauzer o'z serveringizga so'rov yuboradi, u esa kalit bilan tashqi API'ga. Buni backend qismlarida quramiz (Sirlarni boshqarish).

kalit — parol o'rnida. Mashq API'dagi vazifalar/:kalit — ochiq: kalitni bilgan har kim ro'yxatni o'qiydi va o'zgartiradi. Shuning uchun u crypto.randomUUID() — taxmin qilib bo'lmaydigan 122 bitli tasodifiy son. aziz-vazifalari kabi kalitni esa bir daqiqada topishadi. Haqiqiy ilovada kalit o'rnida login va token bo'ladi — Autentifikatsiyali so'rovlar darsida.

9. Ko'p uchraydigan xatolar

9.1 javob.json() ni javob.ok dan oldin, try siz

Server 502 da nginx'ning HTML sahifasini qaytaradi. await javob.json() — SyntaxError: Unexpected token '<', "<html>..." is not valid JSON. Foydalanuvchi tushunarsiz xato ko'radi. Tuzatish: text() + JSON.parse try ichida, JAVOB kodi bilan — yuqoridagi sorovYubor kabi.

9.2 Asosiy URL'dagi qiya chiziq

ASOSIY_URL = ".../api/mashq/" va yol = "/menyu" — natija .../mashq//menyu. Ko'p serverlar buni 404 qiladi. Tuzatish: bitta qoida tanlang va hamma joyda unga amal qiling: asosiy URL chiziqsiz tugaydi, yo'l chiziq bilan boshlanadi. URL'ni xatosiz yig'ish uchun maxsus URL obyekti ham bor — URL va URLSearchParams darsida.

9.3 Bekor qilishni xato deb ko'rsatish

Race condition'dan himoya qo'shdingiz va endi har tez bosishda qizil "So'rov bekor qilindi" chiqyapti. Tuzatish: kod === "BEKOR" bo'lsa, hech narsa ko'rsatmang — bu foydalanuvchining o'z harakati.

9.4 finally siz yuklanmoqda

yuklanmoqda = false ni faqat try oxiriga yozish — xatoda tugma abadiy o'chiq. Tuzatish: finally ga.

9.5 Qatlamni "teshish"

api.js yozildi, lekin shoshilib bitta tinglovchida yana to'g'ridan-to'g'ri fetch yozildi. Endi timeout va xato shakli u yerda yo'q. Tuzatish: qoida — fetch so'zi faqat api.js da uchraydi. VS Code'da Ctrl+Shift+F bilan fetch( ni qidirib tekshirish oson.

10. Mashqlar

1-mashq (oson): menyuniOl

«ApiXatosi va to'liq sorovYubor» bo'limidagi sorovYubor dan foydalanib menyuniOl(turkum) funksiyasini yozing: turkum berilsa — /menyu?turkum=..., berilmasa — /menyu. U taom nomlari massivini qaytarsin. menyuniOl("ichimlik") va menyuniOl() natijasini chiqaring. Kodni qisqa qilish uchun sorovYubor ning faqat javob.ok li oddiy versiyasi («Asosiy URL va javob.ok» bo'limidagi) yetarli.

Yechim
js
const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";

async function sorovYubor(yol) {
  const javob = await fetch(ASOSIY_URL + yol);
  const malumot = await javob.json();
  if (!javob.ok) {
    throw new Error(malumot.xato);
  }
  return malumot;
}

async function menyuniOl(turkum) {
  const yol = turkum ? `/menyu?turkum=${turkum}` : "/menyu";
  const taomlar = await sorovYubor(yol);
  return taomlar.map((t) => t.nom);
}

console.log(await menyuniOl("ichimlik"));
console.log(await menyuniOl());

Konsolda:

text
[ "Ko'k choy" ]
[ 'Osh', "Lag'mon", 'Manti', "Ko'k choy" ]

Node apostrofli satrni qo'sh qo'shtirnoqda ko'rsatadi: "Ko'k choy". menyuniOl fetch ni ham, URL'ni ham bilmaydi — faqat yo'lni. Bu qatlamning o'zi.

2-mashq (o'rta): Faqat kerakli xatoda qayta urinish

qaytaUrin(ish, marta) funksiyasini yozing (Asinxron naqshlar dagi retry). U ish() ni chaqirsin va ApiXatosi ning kod i TARMOQ, VAQT yoki SERVER bo'lsa, 200 × urinish ms kutib qayta urinsin (ko'pi bilan marta marta). Kod TOPILMADI yoki NOTOGRI bo'lsa, darhol xato tashlasin — ularni qayta so'rash befoyda.

Sinov uchun ish sifatida ApiXatosi ni tashlaydigan soxta funksiya yetarli: birinchi ikki chaqiruvda SERVER, uchinchisida "Osh" qaytarsin. Ikkinchi sinov: har doim TOPILMADI tashlasin.

Yechim
js
class ApiXatosi extends Error {
  constructor(xabar, sozlama) {
    super(xabar, sozlama);
    this.name = "ApiXatosi";
    this.holat = sozlama?.holat ?? 0;
    this.kod = sozlama?.kod ?? "NOMALUM";
  }
}

const QAYTA_KODLAR = new Set(["TARMOQ", "VAQT", "SERVER"]);
const kut = (ms) => new Promise((tayyor) => setTimeout(tayyor, ms));

async function qaytaUrin(ish, marta) {
  for (let urinish = 1; ; urinish++) {
    try {
      return await ish();
    } catch (xato) {
      const mumkin = xato instanceof ApiXatosi
        && QAYTA_KODLAR.has(xato.kod);
      if (!mumkin || urinish > marta) throw xato;
      console.log(`${urinish}-urinish: ${xato.kod}, kutamiz`);
      await kut(200 * urinish);
    }
  }
}

let chaqiruv = 0;
const beqaror = async () => {
  chaqiruv++;
  if (chaqiruv < 3) {
    throw new ApiXatosi("Server xatosi", {
      holat: 503, kod: "SERVER",
    });
  }
  return "Osh";
};
console.log("Natija:", await qaytaUrin(beqaror, 3));

try {
  await qaytaUrin(async () => {
    throw new ApiXatosi("Taom topilmadi", {
      holat: 404, kod: "TOPILMADI",
    });
  }, 3);
} catch (xato) {
  console.log("Darhol:", xato.kod);
}

Konsolda:

text
1-urinish: SERVER, kutamiz
2-urinish: SERVER, kutamiz
Natija: Osh
Darhol: TOPILMADI

Sikl for (let urinish = 1; ; urinish++) — shartsiz, undan return yoki throw bilan chiqiladi. Shart urinish > marta — marta ta qayta urinishdan keyin taslim bo'lamiz. Holat 404 ni qayta so'rash befoyda: taom 200 ms da paydo bo'lib qolmaydi.

3-mashq (qiyin): Maydon xatolari

Server 422 da {xato, maydonlar} qaytaradi: {"matn": "1 dan 200 gacha belgi bo'lishi kerak"}. Hozirgi ApiXatosi maydonlar ni yo'qotadi. sorovYubor ni o'zgartiring: ApiXatosi ga uchinchi xususiyat — maydonlar (standarti {}) qo'shilsin va server yuborgan bo'lsa to'ldirilsin. Sinov: /bronlar ga bo'sh JSON ({}) yuborib, xato.kod va maydon nomlarini chiqaring.

Ishora: sorovYubor ga metod va tana ni «Ma'lumot yuborish» bo'limidagidek qo'shing. Object.keys(xato.maydonlar) — maydon nomlari.

Yechim
js
const ASOSIY_URL = "https://ilmhamroh.uz/api/mashq";

class ApiXatosi extends Error {
  constructor(xabar, sozlama) {
    super(xabar, sozlama);
    this.name = "ApiXatosi";
    this.holat = sozlama?.holat ?? 0;
    this.kod = sozlama?.kod ?? "NOMALUM";
    this.maydonlar = sozlama?.maydonlar ?? {};
  }
}

async function sorovYubor(yol, { metod = "GET", tana } = {}) {
  const sozlama = { method: metod, headers: {} };
  if (tana !== undefined) {
    sozlama.headers["Content-Type"] = "application/json";
    sozlama.body = JSON.stringify(tana);
  }
  const javob = await fetch(ASOSIY_URL + yol, sozlama);
  const malumot = await javob.json();
  if (!javob.ok) {
    throw new ApiXatosi(malumot.xato, {
      holat: javob.status,
      kod: javob.status === 422 ? "NOTOGRI" : "HTTP",
      maydonlar: malumot.maydonlar,
    });
  }
  return malumot;
}

try {
  await sorovYubor("/bronlar", { metod: "POST", tana: {} });
} catch (xato) {
  console.log(xato.kod, xato.holat, xato.message);
  console.log(Object.keys(xato.maydonlar));
}

Konsolda:

text
NOTOGRI 422 Ma'lumot noto'g'ri
[ 'ism', 'odamlar', 'sana', 'vaqt' ]

Endi forma har maydon yonida o'z xabarini ko'rsata oladi — Constraint Validation API dagi kabi, faqat xabar serverdan. Server tekshiruvi brauzer tekshiruvining o'rnini bosmaydi va aksincha: brauzerdagisi tezlik uchun, serverdagisi xavfsizlik uchun.

4-mashq: Vazifalar qadami — api.js

vazifalar hozir ma'lumotni faqat xotirada saqlaydi. 32-darsda u serverga ulanadi. Bugun uning "ofitsianti"ni yozamiz — yangi modul assets/js/api.js. Talablar:

  1. Yangi branch: git switch -c feature/api-klient.
  2. export class ApiXatosi extends Error — holat (HTTP kod yoki 0) va kod (TARMOQ, VAQT, BEKOR, TOPILMADI, NOTOGRI, SERVER, HTTP, JAVOB).
  3. export function apiKlient({ asosiyUrl, kalit }) qaytaradigan obyekt — { royxat, qosh, yangila, ochir }. Metodlar:
    • royxat() — GET /vazifalar/:kalit → { vazifalar, versiya }.
    • qosh(matn) — POST → yangi vazifa (id ni server beradi).
    • yangila(id, ozgarish) — PATCH /vazifalar/:kalit/:id ({ bajarildi: true } yoki { matn }) → yangilangan vazifa.
    • ochir(id) — DELETE → null (server 204 qaytaradi).
  4. Har so'rovda AbortSignal.timeout (8 soniya); har metod oxirgi parametr sozlama da signal va kutish qabul qilsin.
  5. tekshiruv/api.js — Node 24 da haqiqiy mashq API bilan sinov.

index.html ga hozircha hech narsa qo'shmang: api.js ni hali hech kim import qilmaydi. Uni 32-darsda asosiy.js ulaydi va o'shanda modulepreload ham qo'shiladi.

Diqqat: Mashq API vazifalarni server xotirasida saqlaydi. Server qayta ishga tushsa yoki ro'yxatga 7 kun tegilmasa — u o'chadi. Sinov va o'rganish uchun bu yetarli; haqiqiy ma'lumot uchun o'z backend'ingiz va bazangiz kerak — 25-qismdan boshlab quramiz.

Yechim
js
// api.js — server bilan gaplashadigan yagona joy
const KUTISH_MS = 8000;

export class ApiXatosi extends Error {
  constructor(xabar, sozlama) {
    super(xabar, sozlama);
    this.name = "ApiXatosi";
    this.holat = sozlama?.holat ?? 0;
    this.kod = sozlama?.kod ?? "NOMALUM";
  }
}

function holatKodi(holat) {
  if (holat === 404) return "TOPILMADI";
  if (holat === 400 || holat === 422) return "NOTOGRI";
  if (holat >= 500) return "SERVER";
  return "HTTP";
}

function tarmoqXatosi(xato) {
  if (xato.name === "TimeoutError") {
    return new ApiXatosi("Server javob bermadi.", {
      kod: "VAQT", cause: xato,
    });
  }
  if (xato.name === "AbortError") {
    return new ApiXatosi("So'rov bekor qilindi.", {
      kod: "BEKOR", cause: xato,
    });
  }
  return new ApiXatosi("Internet aloqasini tekshiring.", {
    kod: "TARMOQ", cause: xato,
  });
}

async function sorovYubor(url, sozlama = {}) {
  const { metod = "GET", tana, signal, kutish = KUTISH_MS } = sozlama;
  const vaqt = AbortSignal.timeout(kutish);
  const sorov = {
    method: metod,
    headers: { Accept: "application/json" },
    signal: signal ? AbortSignal.any([signal, vaqt]) : vaqt,
  };
  if (tana !== undefined) {
    sorov.headers["Content-Type"] = "application/json";
    sorov.body = JSON.stringify(tana);
  }

  let javob;
  let matn;
  try {
    javob = await fetch(url, sorov);
    matn = await javob.text();
  } catch (xato) {
    throw tarmoqXatosi(xato);
  }

  let malumot = null;
  if (matn !== "") {
    try {
      malumot = JSON.parse(matn);
    } catch (xato) {
      throw new ApiXatosi("Server JSON qaytarmadi.", {
        holat: javob.status, kod: "JAVOB", cause: xato,
      });
    }
  }
  if (!javob.ok) {
    const xabar = malumot?.xato ?? `Server xatosi: ${javob.status}`;
    throw new ApiXatosi(xabar, {
      holat: javob.status, kod: holatKodi(javob.status),
    });
  }
  return malumot;
}

export function apiKlient({ asosiyUrl, kalit }) {
  const manzil = `${asosiyUrl}/vazifalar/${kalit}`;
  return {
    royxat: (sozlama) => sorovYubor(manzil, sozlama),
    qosh: (matn, sozlama) => sorovYubor(manzil, {
      ...sozlama, metod: "POST", tana: { matn },
    }),
    yangila: (id, ozgarish, sozlama) =>
      sorovYubor(`${manzil}/${id}`, {
        ...sozlama, metod: "PATCH", tana: ozgarish,
      }),
    ochir: (id, sozlama) => sorovYubor(`${manzil}/${id}`, {
      ...sozlama, metod: "DELETE",
    }),
  };
}

Eksport qilinadigani ikkita: ApiXatosi va apiKlient. sorovYubor, holatKodi, tarmoqXatosi — modulning ichki ishi, tashqaridan ko'rinmaydi. Bu 10-qismdagi "ochiq qism (public API)" g'oyasi.

Accept: "application/json" sarlavhasi serverga "men JSON kutaman" deydi. Mashq API'ga baribir, lekin ba'zi serverlar shunga qarab HTML yoki JSON qaytaradi.

Sinov fayli — tekshiruv/api.js. Har sinov uchun yangi crypto.randomUUID() kalit: ro'yxat har safar bo'sh boshlanadi.

js
// tekshiruv/api.js — api.js haqiqiy mashq API bilan
// Ishga tushirish: node tekshiruv/api.js
import { apiKlient, ApiXatosi } from "../assets/js/api.js";

const api = apiKlient({
  asosiyUrl: "https://ilmhamroh.uz/api/mashq",
  kalit: crypto.randomUUID(),
});

async function xatoniKor(nomi, ish) {
  try {
    await ish();
    console.log(`${nomi}: xato chiqmadi ❌`);
  } catch (xato) {
    const belgi = xato instanceof ApiXatosi ? "✅" : "❌";
    const { kod, holat, message } = xato;
    console.log(`${nomi}: ${belgi} ${kod} ${holat} ${message}`);
  }
}

const bosh = await api.royxat();
console.log("Boshida:", bosh.vazifalar.length, "ta");

const non = await api.qosh("Non olish");
const choy = await api.qosh("Choy damlash");
console.log("Qo'shildi:", non.id, non.matn, "|", choy.id, choy.matn);

const belgilandi = await api.yangila(non.id, { bajarildi: true });
console.log("Belgilandi:", belgilandi.matn, belgilandi.bajarildi);

console.log("O'chirildi:", await api.ochir(choy.id));

const oxiri = await api.royxat();
console.log("Oxirida:", oxiri.vazifalar.map((v) => v.matn));

await xatoniKor("Bo'sh matn", () => api.qosh("   "));
await xatoniKor("Yo'q id", () => api.ochir(999));
await xatoniKor("Vaqt", () => api.royxat({ kutish: 1 }));
await xatoniKor("Bekor", () =>
  api.royxat({ signal: AbortSignal.abort() }));

const yomon = apiKlient({
  asosiyUrl: "https://mavjud-emas.invalid", kalit: "sinov-kalit",
});
await xatoniKor("Tarmoq", () => yomon.royxat());

node tekshiruv/api.js — Konsolda:

text
Boshida: 0 ta
Qo'shildi: 1 Non olish | 2 Choy damlash
Belgilandi: Non olish true
O'chirildi: null
Oxirida: [ 'Non olish' ]
Bo'sh matn: ✅ NOTOGRI 422 Ma'lumot noto'g'ri
Yo'q id: ✅ TOPILMADI 404 Vazifa topilmadi
Vaqt: ✅ VAQT 0 Server javob bermadi.
Bekor: ✅ BEKOR 0 So'rov bekor qilindi.
Tarmoq: ✅ TARMOQ 0 Internet aloqasini tekshiring.

Hamma muvaffaqiyatsizlik turi bitta ApiXatosi bo'lib keldi — interfeys uchun shu kerak edi. Sozlama kutish: 1 — 1 ms timeout, hech bir server ulgurmaydi: VAQT ni sinashning eng oddiy yo'li. Metod AbortSignal.abort() — allaqachon bekor qilingan signal. Domen .invalid internetda hech qachon mavjud bo'lmaydi (RFC 2606) — tarmoq xatosi kafolatlangan.

bash
git add assets/js/api.js tekshiruv/api.js
git commit -m "API klient: ApiXatosi va apiKlient"
git switch main
git merge feature/api-klient
git branch -d feature/api-klient
git push

11. Real ishda

  • Har jiddiy frontend loyihada api/ papkasi yoki apiClient.ts fayli bor. Payme, Uzum, Click'ning veb-ilovalarida ham interfeys fetch ni to'g'ridan-to'g'ri chaqirmaydi — biror klient orqali.
  • Tayyor klientlar. Ko'p jamoalar o'ramni o'zi yozmaydi, tayyor kutubxona oladi: Axios (keyingi dars), ky, ofetch. Ular bugungi g'oyalarning hammasini — asosiy URL, timeout, xato obyekti — tayyor beradi.
  • Server holati kutubxonalari. React'da "yuklanmoqda / xato / ma'lumot" holatlari, keshlash va race condition'dan himoyani TanStack Query avtomatik bajaradi (TanStack Query I). U ham ichida aynan siz yozgan klient funksiyalarini chaqiradi.
  • TypeScript. Klient qatlami — serverdan kelgan "noma'lum" ma'lumotga tur beriladigan joy (API ma'lumotlarini tiplash).
  • Intervyu. "Race condition'ni qanday oldini olasiz?", "Fetch xatolarini qanday boshqarasiz?" — frontend intervyularida juda ko'p so'raladi.

Xulosa

  • API klient qatlami — fetch uchraydigan yagona joy: asosiy URL, JSON, javob.ok, timeout bir marta yoziladi.
  • Besh xil muvaffaqiyatsizlik (tarmoq, timeout, bekor, HTTP xato, buzuq JSON) — bitta ApiXatosi (holat, kod, message) ga keltiriladi; asl xato — cause da.
  • AbortSignal.timeout + AbortSignal.any — timeout va tashqi bekor qilish birga.
  • Interfeysda uch holat: yuklanmoqda (tugma o'chiq), xato (xabar + qayta urinish), ma'lumot; finally holatni albatta tozalaydi.
  • Race condition — eski javob yangisini bosib ketadi; yechim: eskisini abort() yoki so'rov raqami.
  • Server matni — faqat textContent; maxfiy kalit — hech qachon frontend'da emas.

Keyingi dars: Axios — bugun qo'lda yozgan klientimizning mashhur tayyor varianti: instance, baseURL, interceptor'lar va error.response.

Manbalar

  • MDN: "Using the Fetch API", "AbortSignal.timeout()", "AbortSignal.any()" — developer.mozilla.org
  • WHATWG Fetch Standard — fetch.spec.whatwg.org
  • RFC 2606: "Reserved Top Level DNS Names" (.invalid) — rfc-editor.org
  • Kent C. Dodds, "Replace axios with a simple custom fetch wrapper" — kentcdodds.com/blog
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
API klient qatlami: hamma fetch larni bitta joyga yig'ish — IlmHamroh