20-bo‘lim
Amaliy loyiha - kutubxona API
O'rganilgan hamma narsani birlashtirib, bazaga yozadigan, tekshiradigan, himoyalangan va testlar bilan qoplangan REST API yozamiz.
Ushbu bo‘lim mundarijasi
Endi yigirma bo'limda o'rgangan narsalarni bitta ishlaydigan loyihaga yig'amiz: kutubxona API si.
U kitoblarni saqlaydi, qidiradi, tekshiradi, xatolarni to'g'ri qaytaradi va testlar bilan qoplangan.
Loyiha tuzilishi #
Xatolar #
export class ApiXatosi extends Error {
constructor(holat, kod, xabar, tafsilot = null) {
super(xabar);
this.name = "ApiXatosi";
this.holat = holat;
this.kod = kod;
this.tafsilot = tafsilot;
}
}
export const topilmadi = (nima) =>
new ApiXatosi(404, "TOPILMADI", `${nima} topilmadi`);
export const tekshiruvXatosi = (tafsilot) =>
new ApiXatosi(422, "TEKSHIRUV", "maydonlar noto'g'ri", tafsilot);
export function xatoQatlami(xato, sorov, javob, keyingi) {
if (xato instanceof ApiXatosi) {
javob.status(xato.holat).json({
xato: { kod: xato.kod, xabar: xato.message,
tafsilot: xato.tafsilot },
});
return;
}
if (xato instanceof SyntaxError && "body" in xato) {
javob.status(400).json({
xato: { kod: "BUZUQ_JSON", xabar: "JSON o'qib bo'lmadi",
tafsilot: null },
});
return;
}
javob.status(500).json({
xato: { kod: "ICHKI_XATO", xabar: "kutilmagan xato",
tafsilot: null },
});
}
15-bo'limdagi izchil xato shakli - endi alohida modulda.
xatoQatlami uch holatni ajratadi: bizning xatolarimiz, buzuq
JSON va qolgan hamma narsa. Oxirgisida tafsilot berilmaydi.
Baza qatlami #
import { DatabaseSync } from "node:sqlite";
export function bazaYarat(yol = ":memory:") {
const baza = new DatabaseSync(yol);
baza.exec(`
CREATE TABLE IF NOT EXISTS kitoblar (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sarlavha TEXT NOT NULL,
muallif TEXT NOT NULL,
yil INTEGER NOT NULL,
nusxalar INTEGER NOT NULL DEFAULT 1
)
`);
const soragich = {
hammasi: baza.prepare(
`SELECT * FROM kitoblar
WHERE (:muallif IS NULL OR muallif = :muallif)
ORDER BY id LIMIT :hajm OFFSET :sakra`,
),
soni: baza.prepare(
`SELECT COUNT(*) AS soni FROM kitoblar
WHERE (:muallif IS NULL OR muallif = :muallif)`,
),
bittasi: baza.prepare("SELECT * FROM kitoblar WHERE id = ?"),
qoshish: baza.prepare(
`INSERT INTO kitoblar (sarlavha, muallif, yil, nusxalar)
VALUES (:sarlavha, :muallif, :yil, :nusxalar)`,
),
ochirish: baza.prepare("DELETE FROM kitoblar WHERE id = ?"),
};
return {
royxat({ muallif = null, sahifa = 1, hajm = 10 }) {
const sakra = (sahifa - 1) * hajm;
const qatorlar = soragich.hammasi.all({ muallif, hajm, sakra });
const { soni } = soragich.soni.get({ muallif });
return { qatorlar: qatorlar.map((q) => ({ ...q })), jami: soni };
},
topish(id) {
const qator = soragich.bittasi.get(id);
return qator ? { ...qator } : null;
},
qoshish(kitob) {
const { lastInsertRowid } = soragich.qoshish.run(kitob);
return this.topish(lastInsertRowid);
},
ochirish(id) {
return soragich.ochirish.run(id).changes > 0;
},
yopish() {
baza.close();
},
};
}
Baza bilan ishlash butunlay shu faylda. Qolgan kod SQL
ko'rmaydi - u faqat royxat, topish, qoshish metodlarini
biladi.
Foydasi: ertaga SQLite dan PostgreSQL ga o'tsangiz, faqat shu fayl o'zgaradi.
Tekshiruv #
const HOZIRGI_YIL = 2026;
export function kitobniTekshir(malumot) {
const xatolar = {};
const sarlavha = String(malumot.sarlavha ?? "").trim();
if (sarlavha.length < 1) {
xatolar.sarlavha = "sarlavha bo'sh bo'lmasin";
} else if (sarlavha.length > 200) {
xatolar.sarlavha = "sarlavha 200 belgidan oshmasin";
}
const muallif = String(malumot.muallif ?? "").trim();
if (muallif.length < 2) {
xatolar.muallif = "muallif kamida 2 belgi bo'lsin";
}
const yil = Number(malumot.yil);
if (!Number.isInteger(yil) || yil < 1450 || yil > HOZIRGI_YIL) {
xatolar.yil = `yil 1450 va ${HOZIRGI_YIL} orasida bo'lsin`;
}
const nusxalar = Number(malumot.nusxalar ?? 1);
if (!Number.isInteger(nusxalar) || nusxalar < 0) {
xatolar.nusxalar = "nusxalar manfiy bo'lmagan butun son bo'lsin";
}
return { toza: { sarlavha, muallif, yil, nusxalar }, xatolar };
}
export function idniTekshir(xom) {
const id = Number(xom);
return Number.isInteger(id) && id > 0 ? id : null;
}
14-bo'limdagi uch tamoyil: tozalash, aylantirish, hamma xatoni yig'ish.
E'tibor bering, toza faqat ruxsat etilgan to'rt maydondan
iborat - mijoz id yoki boshqa narsa yuborsa, u o'tmaydi.
Marshrutlar #
import { Router } from "express";
import { topilmadi, tekshiruvXatosi } from "./loyiha-xatolar.mjs";
import { kitobniTekshir, idniTekshir } from "./loyiha-tekshiruv.mjs";
export function kitobMarshruti(ombor) {
const marshrut = Router();
marshrut.get("/", (sorov, javob) => {
const sahifa = Math.max(1, Number(sorov.query.sahifa ?? 1));
const xomHajm = Number(sorov.query.hajm ?? 10);
const hajm = Math.min(50, Math.max(1, xomHajm));
const muallif = sorov.query.muallif?.trim() || null;
const { qatorlar, jami } = ombor.royxat({ muallif, sahifa, hajm });
javob.json({
malumot: qatorlar,
sahifalash: { sahifa, hajm, jami,
sahifalar: Math.ceil(jami / hajm) },
});
});
marshrut.get("/:id", (sorov, javob) => {
const id = idniTekshir(sorov.params.id);
if (id === null) {
throw tekshiruvXatosi({ id: "id musbat son bo'lsin" });
}
const kitob = ombor.topish(id);
if (!kitob) throw topilmadi("kitob");
javob.json({ malumot: kitob });
});
marshrut.post("/", (sorov, javob) => {
const { toza, xatolar } = kitobniTekshir(sorov.body ?? {});
if (Object.keys(xatolar).length > 0) throw tekshiruvXatosi(xatolar);
const kitob = ombor.qoshish(toza);
javob.status(201).location(`/kitoblar/${kitob.id}`)
.json({ malumot: kitob });
});
marshrut.delete("/:id", (sorov, javob) => {
const id = idniTekshir(sorov.params.id);
if (id === null) {
throw tekshiruvXatosi({ id: "id musbat son bo'lsin" });
}
if (!ombor.ochirish(id)) throw topilmadi("kitob");
javob.status(204).end();
});
return marshrut;
}
Marshrutlar faqat so'rovni o'qiydi va javob beradi. Tekshiruv alohida modulda, baza alohida modulda.
Diqqat: xatolar throw qilinmoqda, keyingi(xato) emas - Express
5 buni o'zi ushlaydi (11-bo'lim).
Ilovani yig'ish #
import express from "express";
import { kitobMarshruti } from "./loyiha-marshrut.mjs";
import { xatoQatlami } from "./loyiha-xatolar.mjs";
export function ilovaYarat(ombor) {
const app = express();
app.disable("x-powered-by");
app.use((sorov, javob, keyingi) => {
javob.set({
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
});
keyingi();
});
app.use(express.json({ limit: "100kb" }));
app.get("/holat", (sorov, javob) => {
javob.json({ holat: "ishlayapti" });
});
app.use("/kitoblar", kitobMarshruti(ombor));
app.use((sorov, javob) => {
javob.status(404).json({
xato: { kod: "YOL_TOPILMADI", xabar: "bunday yo'l yo'q",
tafsilot: null },
});
});
app.use(xatoQatlami);
return app;
}
Tartib 13-bo'limdagi qoidaga amal qiladi: xavfsizlik, tahlil, marshrutlar, 404, xato qatlami.
Ishlatib ko'ramiz #
import { bazaYarat } from "./loyiha-baza.mjs";
import { ilovaYarat } from "./loyiha-ilova.mjs";
const ombor = bazaYarat();
const server = ilovaYarat(ombor).listen(0);
const asos = `http://localhost:${server.address().port}`;
const sora = async (yol, sozlama = {}) => {
const j = await fetch(asos + yol, {
headers: { "Content-Type": "application/json" },
...sozlama,
});
const matn = await j.text();
return { holat: j.status, tana: matn ? JSON.parse(matn) : null };
};
const qosh = (kitob) =>
sora("/kitoblar", { method: "POST", body: JSON.stringify(kitob) });
console.log("holat:", (await sora("/holat")).tana);
const birinchi = await qosh({
sarlavha: "O'tkan kunlar", muallif: "Abdulla Qodiriy",
yil: 1926, nusxalar: 5,
});
console.log("yaratildi:", birinchi.holat, birinchi.tana.malumot);
await qosh({
sarlavha: "Mehrobdan chayon", muallif: "Abdulla Qodiriy",
yil: 1929, nusxalar: 3,
});
await qosh({
sarlavha: "Sarob", muallif: "Abdulla Qahhor", yil: 1935,
});
const royxat = await sora("/kitoblar");
console.log("jami:", royxat.tana.sahifalash.jami);
console.log("sarlavhalar:", royxat.tana.malumot.map((k) => k.sarlavha));
const filtr = await sora("/kitoblar?muallif=Abdulla%20Qahhor");
console.log("filtrlangan:", filtr.tana.malumot.length);
server.close();
ombor.yopish();
holat: { holat: 'ishlayapti' }
yaratildi: 201 {
id: 1,
sarlavha: "O'tkan kunlar",
muallif: 'Abdulla Qodiriy',
yil: 1926,
nusxalar: 5
}
jami: 3
sarlavhalar: [ "O'tkan kunlar", 'Mehrobdan chayon', 'Sarob' ]
filtrlangan: 1
Uchinchi kitobda nusxalar berilmadi - tekshiruv unga odatiy 1
qiymatini berdi.
Xato yo'llarini sinash #
import { bazaYarat } from "./loyiha-baza.mjs";
import { ilovaYarat } from "./loyiha-ilova.mjs";
const ombor = bazaYarat();
const server = ilovaYarat(ombor).listen(0);
const asos = `http://localhost:${server.address().port}`;
const sora = async (yol, sozlama = {}) => {
const j = await fetch(asos + yol, {
headers: { "Content-Type": "application/json" },
...sozlama,
});
const matn = await j.text();
return { holat: j.status, tana: matn ? JSON.parse(matn) : null };
};
const yoq = await sora("/kitoblar/999");
console.log("yo'q kitob:", yoq.holat, yoq.tana.xato.kod);
const buzuqId = await sora("/kitoblar/salom");
console.log("buzuq id:", buzuqId.holat, buzuqId.tana.xato.kod);
const notogri = await sora("/kitoblar", {
method: "POST",
body: JSON.stringify({ sarlavha: "", muallif: "X", yil: 3000 }),
});
console.log("tekshiruv:", notogri.holat);
console.log("maydonlar:", Object.keys(notogri.tana.xato.tafsilot).sort());
const buzuqJson = await sora("/kitoblar", {
method: "POST",
body: "{ bu json emas",
});
console.log("buzuq json:", buzuqJson.holat, buzuqJson.tana.xato.kod);
const yolYoq = await sora("/bunday-yol-yoq");
console.log("yo'l yo'q:", yolYoq.holat, yolYoq.tana.xato.kod);
server.close();
ombor.yopish();
yo'q kitob: 404 TOPILMADI
buzuq id: 422 TEKSHIRUV
tekshiruv: 422
maydonlar: [ 'muallif', 'sarlavha', 'yil' ]
buzuq json: 400 BUZUQ_JSON
yo'l yo'q: 404 YOL_TOPILMADI
Har bir xato o'z kodi bilan qaytdi va shakl hamma joyda bir xil.
Testlar #
import { run } from "node:test";
import { writeFile, rm } from "node:fs/promises";
await writeFile("kutubxona.test.mjs", `
import { test, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import { bazaYarat } from "./loyiha-baza.mjs";
import { ilovaYarat } from "./loyiha-ilova.mjs";
let ombor;
let server;
let asos;
beforeEach(() => {
ombor = bazaYarat();
server = ilovaYarat(ombor).listen(0);
asos = "http://localhost:" + server.address().port;
});
afterEach(() => {
server.close();
ombor.yopish();
});
const qosh = (tana) => fetch(asos + "/kitoblar", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(tana),
});
test("yangi kitob 201 bilan yaratiladi", async () => {
const javob = await qosh({ sarlavha: "Sarob", muallif: "Qahhor",
yil: 1935 });
assert.equal(javob.status, 201);
assert.match(javob.headers.get("location"), /^\\/kitoblar\\/\\d+$/);
});
test("bo'sh sarlavha rad etiladi", async () => {
const javob = await qosh({ sarlavha: "", muallif: "Qahhor", yil: 1935 });
assert.equal(javob.status, 422);
const tana = await javob.json();
assert.ok("sarlavha" in tana.xato.tafsilot);
});
test("kelajakdagi yil rad etiladi", async () => {
const javob = await qosh({ sarlavha: "X", muallif: "Yy", yil: 2999 });
assert.equal(javob.status, 422);
});
test("har bir test toza bazadan boshlanadi", async () => {
const javob = await fetch(asos + "/kitoblar");
const tana = await javob.json();
assert.equal(tana.sahifalash.jami, 0);
});
test("o'chirilgan kitob 404 qaytaradi", async () => {
const yaratildi = await (await qosh({ sarlavha: "X", muallif: "Yy",
yil: 2000 })).json();
const id = yaratildi.malumot.id;
const ochirildi = await fetch(asos + "/kitoblar/" + id,
{ method: "DELETE" });
assert.equal(ochirildi.status, 204);
const qayta = await fetch(asos + "/kitoblar/" + id);
assert.equal(qayta.status, 404);
});
`);
let otdi = 0;
let yiqildi = 0;
const nomlar = [];
for await (const hodisa of run({ files: ["kutubxona.test.mjs"] })) {
if (hodisa.type === "test:pass") {
otdi++;
nomlar.push(hodisa.data.name);
}
if (hodisa.type === "test:fail") yiqildi++;
}
console.log(nomlar.join("\n"));
console.log(`O'tdi: ${otdi} | Yiqildi: ${yiqildi}`);
await rm("kutubxona.test.mjs");
yangi kitob 201 bilan yaratiladi
bo'sh sarlavha rad etiladi
kelajakdagi yil rad etiladi
har bir test toza bazadan boshlanadi
o'chirilgan kitob 404 qaytaradi
O'tdi: 5 | Yiqildi: 0
To'rtinchi test muhim: u beforeEach haqiqatan toza baza
berayotganini isbotlaydi. Oldingi testlar kitob qo'shgan bo'lsa
ham, bu yerda jami nol.
Server va muloyim to'xtash #
import { bazaYarat } from "./loyiha-baza.mjs";
import { ilovaYarat } from "./loyiha-ilova.mjs";
const ombor = bazaYarat(process.env.BAZA_YOLI ?? "kutubxona.db");
const port = Number(process.env.PORT ?? 3000);
const server = ilovaYarat(ombor).listen(port, () => {
console.log(`Server ${port} portda ishlayapti`);
});
function toxtat(signal) {
console.log(`${signal} keldi, to'xtatilmoqda...`);
server.close(() => {
ombor.yopish();
console.log("Toza chiqildi");
process.exit(0);
});
setTimeout(() => {
console.error("Muddat tugadi, majburan chiqilmoqda");
process.exit(1);
}, 10_000).unref();
}
process.on("SIGINT", () => toxtat("SIGINT"));
process.on("SIGTERM", () => toxtat("SIGTERM"));
Ctrl+C bosilganda yoki server yangilanganda dastur darhol
o'lsa, o'sha paytdagi so'rovlar yarim yo'lda uziladi.
server.close() esa yangi ulanishni qabul qilmaydi, lekin
boshlangan so'rovlarni tugatadi. Shundan keyingina baza
yopiladi.
setTimeout(...).unref() - himoya chorasi: agar biror so'rov
osilib qolsa, 10 soniyadan keyin baribir chiqiladi. unref()
esa bu taymer dasturni ushlab turmasligini ta'minlaydi.
Bu 6-bo'limdagi SIGINT va process.exit darslarining amaliy
qo'llanishi.
Nimalar ishlatildi #
| Qism | Bo'lim |
|---|---|
| ESM modullar | 3 |
node:sqlite | 16 |
Hodisalar sikli va async | 7 |
| Express, marshrut, qatlam | 11-13 |
| Tekshiruv va 422 | 14 |
| REST va sahifalash | 15 |
| Xavfsizlik sarlavhalari | 18 |
node:test va beforeEach | 19 |
SIGINT va muloyim to'xtash | 6 |
Bu loyihani haqiqiy xizmatga aylantirish uchun:
- Autentifikatsiya - 17-bo'limdagi sessiyalar bilan yozish amallarini himoyalang.
- Jurnal - har bir so'rovni yozing, lekin maxfiy ma'lumotsiz (18-bo'lim).
- So'rovlarni cheklash - 18-bo'limdagi qatlam.
- Migratsiyalar - jadval o'zgarishini tartibli boshqarish.
- Docker - bitta buyruq bilan ishga tushirish.
- CI - har bir o'zgarishda
node --testavtomatik ishlasin.
- Loyihani fayl-fayl ko'chirib yozing.
nusxalarmaydoniga odatiy qiymat berilishini tekshiring.PATCHmarshrutini qo'shing.- Sarlavha bo'yicha qidiruv qo'shing.
- Ikkita kitob bir xil sarlavhada bo'lmasin degan qoida qo'ying.
- Sahifalash chegarasini sinab ko'ring.
- Yangi tekshiruv qoidasi uchun test yozing.
- Xato kodlarini ro'yxat qilib hujjatlashtiring.
- 17-bo'limdagi himoya qatlamini
POSTga qo'shing. - Muloyim to'xtashni
Ctrl+Cbilan sinang.
Xulosa #
- Har bir fayl - bitta vazifa: baza, tekshiruv, marshrut, xato, ilova, server.
- SQL faqat baza modulida - bazani almashtirish oson bo'ladi.
- Ilova portni tinglamaydi; uni server fayli qiladi - shuning uchun test oson.
- Tekshiruv ruxsat etilgan maydonlarni qaytaradi.
- Hamma xato bir xil shaklda; 500 da tafsilot berilmaydi.
- Qatlamlar tartibi: xavfsizlik, tahlil, marshrut, 404, xato.
beforeEachhar bir testga toza baza beradi.- Muloyim to'xtash boshlangan so'rovlarni tugatib, keyin chiqadi.
Tabriklaymiz - siz Node.js ning asosiy qismini o'rgandingiz. Endi eng yaxshi davom etish yo'li - o'z loyihangizni yozish.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.