20-bo‘lim

Amaliy loyiha - kutubxona API

O'rganilgan hamma narsani birlashtirib, bazaga yozadigan, tekshiradigan, himoyalangan va testlar bilan qoplangan REST API yozamiz.

🕑 9 daqiqa o‘qish 📄 570 so‘z 👁 0 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Loyiha tuzilishi
  2. Xatolar
  3. Baza qatlami
  4. Tekshiruv
  5. Marshrutlar
  6. Ilovani yig'ish
  7. Ishlatib ko'ramiz
  8. Xato yo'llarini sinash
  9. Testlar
  10. Server va muloyim to'xtash
  11. Nimalar ishlatildi
  12. Xulosa

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 #

Har bir fayl - bitta vazifa server.mjs portni tinglaydi ilova.mjs qatlamlarni yig'adi marshrut.mjs tekshiruv.mjs xatolar.mjs baza.mjs ilova.mjs portni tinglamaydi - shuning uchun uni testdan chaqirish oson

Xatolar #

JavaScript
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 #

JavaScript
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 #

JavaScript
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 #

JavaScript
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 #

JavaScript
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 #

JavaScript
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();
Natija
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 #

JavaScript
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();
Natija
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 #

JavaScript
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");
Natija
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 #

JavaScript
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"));
Muloyim to'xtash nima uchun kerak

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 #

QismBo'lim
ESM modullar3
node:sqlite16
Hodisalar sikli va async7
Express, marshrut, qatlam11-13
Tekshiruv va 42214
REST va sahifalash15
Xavfsizlik sarlavhalari18
node:test va beforeEach19
SIGINT va muloyim to'xtash6
Keyingi qadamlar

Bu loyihani haqiqiy xizmatga aylantirish uchun:

  1. Autentifikatsiya - 17-bo'limdagi sessiyalar bilan yozish amallarini himoyalang.
  2. Jurnal - har bir so'rovni yozing, lekin maxfiy ma'lumotsiz (18-bo'lim).
  3. So'rovlarni cheklash - 18-bo'limdagi qatlam.
  4. Migratsiyalar - jadval o'zgarishini tartibli boshqarish.
  5. Docker - bitta buyruq bilan ishga tushirish.
  6. CI - har bir o'zgarishda node --test avtomatik ishlasin.
Amaliy topshiriq
  1. Loyihani fayl-fayl ko'chirib yozing.
  2. nusxalar maydoniga odatiy qiymat berilishini tekshiring.
  3. PATCH marshrutini qo'shing.
  4. Sarlavha bo'yicha qidiruv qo'shing.
  5. Ikkita kitob bir xil sarlavhada bo'lmasin degan qoida qo'ying.
  6. Sahifalash chegarasini sinab ko'ring.
  7. Yangi tekshiruv qoidasi uchun test yozing.
  8. Xato kodlarini ro'yxat qilib hujjatlashtiring.
  9. 17-bo'limdagi himoya qatlamini POST ga qo'shing.
  10. Muloyim to'xtashni Ctrl+C bilan 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.
  • beforeEach har 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.

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.