Mundarija (28)
- Bu darsda
- 1. Nega bu kerak?
- 2. express.Router
- 2.1 Mini-ilova
- 2.2 mergeParams — ichma-ich routerlar
- 3. Qatlamlar
- 3.1 Uch qatlam va ularning chegarasi
- 3.2 Feature-folder
- 4. Repository: interfeys va amalga oshirish
- 4.1 VazifaOmbori interfeysi
- 4.2 XotiraOmbori
- 5. Xizmat va DI framework'siz
- 5.1 vazifaXizmati(ombor)
- 5.2 Dependency injection nima
- 6. Marshrut (controller) va yakuniy ilova.ts
- 6.1 vazifalarRouteri(xizmat)
- 6.2 Yakuniy ilova.ts
- 7. Hujumchi nigohi
- 8. API qadami: Router va qatlamlar (holat 13, commit)
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): to'ldiring
- 2-mashq (o'rta): qoida xizmatda
- 3-mashq (qiyin): ichma-ich router
- 4-mashq: API qadami
- 11. Real ishda
- Xulosa
- Manbalar
Express Router va qatlamli arxitektura: marshrut, xizmat va repository (ombor)
Qisqacha:
express.Router()— kichik, alohida ulanadigan "mini-ilova": o'z marshrutlari va middleware'lari bor,app.use("/vazifalar", router)bilan ulanadi. Katta loyihada kod feature (vazifalar, foydalanuvchilar) bo'yicha papkalarga, har feature ichida esa qatlamlarga bo'linadi. Marshrut (controller) HTTP'ni biladi. Xizmat (service) biznes qoidalarini biladi. Repository (ombor) ma'lumot qayerda saqlanishini biladi. Qatlamlar bir-birini parametr orqali oladi — bu framework'siz dependency injection.vazifalar-apidaVazifaOmboriinterfeysi shunday paydo bo'ladi. 26-qismda Postgres faqat yangi ombor sifatida qo'shiladi, marshrut va xizmat o'zgarmaydi.
Bu darsda
express.Router()bilan marshrutlarni alohida faylga ajratasiz;req.baseUrlvamergeParamsni ishlatasiz.- Feature-folder tuzilmasini va qatlamlarni (marshrut → xizmat → ombor) tushuntirasiz.
VazifaOmboriinterfeysini va uning xotiradagi amalga oshirilishini yozasiz.- Dependency injection'ni factory funksiyalar bilan, kutubxonasiz qilasiz.
vazifalar-apini holat 13 ga yetkazib, commit qilasiz.
Oldin bilishingiz kerak: Middleware va request lifecycle, res obyekti, req obyekti, type va interface.
1. Nega bu kerak?
Express'ga kirish darsida ko'chirgan src/ilova.ts hozir 140 qatorga yaqin. Ichida hammasi aralash: Express sozlamalari, idniOl, marshrutlar, "ro'yxat to'lganmi" kabi qoidalar, xato ishlovchi. Yangi resurs (/teglar, /eksportlar) qo'shilsa — 300 qator. Uch muammo tug'iladi:
- O'qish qiyin. "Vazifa yaratish qoidasi qayerda?" — fayl bo'ylab qidirasiz.
- Test qiyin. "1000 tadan ko'p vazifa bo'lmasin" qoidasini sinash uchun HTTP server ochish, 1000 ta so'rov yuborish kerak.
- O'zgartirish qiyin. 26-qismda ma'lumot xotiradan Postgres'ga ko'chadi.
ombor.yarat(...)chaqiruvlari marshrutlar ichida tarqalgan — hammasini qayta yozish kerak.
Yechim — kodni mas'uliyat bo'yicha bo'lish. O'xshatish: restoran. Ofitsiant (marshrut) mehmon bilan gaplashadi: buyurtmani oladi, hisobni beradi. Oshpaz (xizmat) taomni qoidalar bo'yicha tayyorlaydi: "osh faqat 12:00 gacha". Omborchi (repository) mahsulot qayerdaligini biladi: muzlatgich, yerto'la yoki bozordan keltirish. Ofitsiant muzlatgichga kirmaydi, oshpaz mehmonga hisob yozmaydi. Omborni yangi binoga ko'chirsangiz — ofitsiant ham, oshpaz ham ishlayveradi.
flowchart TD
H["HTTP so'rov"] --> A["ilova.ts<br/>middleware, 404, xatolar"]
A --> M["marshrut.ts<br/>req → xizmat → res"]
M --> X["xizmat.ts<br/>biznes qoidalari"]
X --> I["ombor.ts<br/>interface VazifaOmbori"]
I --> XO["xotira-ombor.ts<br/>(bugun)"]
I -.-> PG["pg ombor<br/>(26-qism)"]O'qlar faqat pastga yo'naladi: har qatlam faqat o'zidan pastdagini biladi. Xizmat req va res ni ko'rmaydi, ombor esa HTTP status kodlarini bilmaydi. Uzuq chiziq — kelajak: interfeysning ikkinchi amalga oshirilishi.
2. express.Router
2.1 Mini-ilova
Router() — app ga o'xshash obyekt: get, post, use, route metodlari bor, lekin listen yo'q. U middleware sifatida ilovaga ulanadi:
const r = Router();
r.get("/", royxatIshlovchi); // GET /vazifalar
r.get("/:id", bittaIshlovchi); // GET /vazifalar/7
app.use("/vazifalar", r);Router ichida yo'llar nisbiy: / va /:id. /vazifalar qismini app.use beradi. Middleware darsidagi izni eslang: app.use("/a", …) ichida req.url — /b, req.baseUrl — /a. Router xuddi shu mexanizm bilan ishlaydi.
Foydasi: routerni boshqa yo'lga ko'chirish bitta qator. Versiyalash darsida aynan shunday bo'ladi: /vazifalar → /v1/vazifalar. Router ichidagi kod o'zgarmaydi — agar u yo'lni qattiq yozmasa. Shuning uchun Location sarlavhasi endi req.baseUrl dan yasaladi:
res.status(201).location(`${req.baseUrl}/${String(v.id)}`).json(v);2.2 mergeParams — ichma-ich routerlar
Ba'zan resurs boshqasining ichida bo'ladi: vazifaning izohlari — /vazifalar/7/izohlar. Izohlar routeri alohida, lekin unga :id kerak. mergeParams ("parametrlarni qo'shish") — routerga ota yo'ldagi parametrlarni ham ko'rsatadigan sozlama. Biz ishga tushirdik (Express 5.3.0) — bir xil router, mergeParams siz va bilan. Chiqishda params ga qarang:
function izohlarRouteri(mergeParams) {
const r = express.Router({ mergeParams });
r.get("/", (req, res) => {
const { params, baseUrl, url } = req;
res.json({ params, baseUrl, url });
});
return r;
}
app.use("/a/vazifalar/:id/izohlar", izohlarRouteri(false));
app.use("/b/vazifalar/:id/izohlar", izohlarRouteri(true));/a {"params":{},"baseUrl":"/a/vazifalar/7/izohlar","url":"/"}
/b {"params":{"id":"7"},"baseUrl":"/b/vazifalar/7/izohlar","url":"/"}Sukut bo'yicha router faqat o'z yo'l parametrlarini ko'radi — params bo'sh. mergeParams: true — ota yo'ldagi :id ham req.params ga qo'shiladi. Bu xato topish qiyin: req.params.id jim undefined bo'ladi.
vazifalar-api da izohlar yo'q va kanon ichma-ich resurslardan qochadi: teg — alohida resurs emas, filtr (?teg=uy). Sabab va qoidalar — REST prinsiplari darsida.
Tekshirib ko'ring: Router
app.use("/vazifalar", r)bilan ulangan. Ichidar.get("/vazifalar/:id", …)yozilgan.GET /vazifalar/7qaysi ishlovchiga boradi?
Javob
Hech qaysiga — 404. Router ichida yo'l /7 bo'lib keladi, ishlovchi esa /vazifalar/:id ni kutadi. U faqat /vazifalar/vazifalar/7 ga javob beradi. Router ichidagi yo'llar — app.use dagi qismsiz.
3. Qatlamlar
3.1 Uch qatlam va ularning chegarasi
| Qatlam | Fayl | Biladi | Bilmaydi |
|---|---|---|---|
| Marshrut (controller) | marshrut.ts |
req, res, status, sarlavhalar |
ma'lumot qayerda saqlanadi |
| Xizmat (service) | xizmat.ts |
biznes qoidalari: 404, 1000 limiti | req, res, Express |
| Repository (ombor) | ombor.ts, xotira-ombor.ts |
saqlash: Map, fayl, SQL | HTTP, qoidalar |
Controller — "boshqaruvchi": so'rovni tahlil qiladi, xizmatni chaqiradi, javobni yozadi. Kursda u marshrut.ts. Service (xizmat) — ilovaning "aqli". Repository (ombor) — saqlash qatlami; nomi "omborxona" ma'nosida.
Bu naqsh — qatlamli arxitektura (layered architecture). U MVC, Clean Architecture, hexagonal kabi nomlar bilan ham uchraydi. Ularning hammasida asosiy g'oya bir: biznes qoidalari HTTP'dan ham, bazadan ham mustaqil.
3.2 Feature-folder
Fayllarni qanday papkalarga joylash kerak? Ikki yo'l:
| Turi bo'yicha | Feature bo'yicha |
|---|---|
controllers/vazifalar.ts |
vazifalar/marshrut.ts |
services/vazifalar.ts |
vazifalar/xizmat.ts |
repositories/vazifalar.ts |
vazifalar/ombor.ts |
Kanon ikkinchisini tanladi — feature-folder. "Vazifalar" bilan ishlaganda kerakli hamma fayl bitta papkada. Yangi feature (eksport/) — yangi papka. Umumiy narsalar (HttpXato, jsonKerak) — umumiy/ da. NestJS ham shu yo'ldan boradi (modul — feature).
4. Repository: interfeys va amalga oshirish
4.1 VazifaOmbori interfeysi
Xizmat konkret klassni emas, interfeysni biladi (interfeyslar darsi): "menga royxat, ol, yarat, yangila, ochir qila oladigan narsa bering". Kanon fayli to'liq:
// ombor.ts — repository interfeysi: xizmat saqlash qanday
// ishlashini bilmaydi. Hamma metod async — 26-qismda Postgres (pg)
// bilan almashganda chaqiruvchi kod o'zgarmaydi
import type {
Vazifa, VazifaOzgarishi, YangiVazifa,
} from "./vazifa.ts";
export type RoyxatSorovi = { bajarildi?: boolean };
export interface VazifaOmbori {
royxat(sorov: RoyxatSorovi): Promise<Vazifa[]>;
ol(id: number): Promise<Vazifa | undefined>;
yarat(yangi: YangiVazifa): Promise<Vazifa>;
/** @returns undefined — bunday id yo'q */
yangila(id: number, ozgarish: VazifaOzgarishi):
Promise<Vazifa | undefined>;
/** @returns false — bunday id yo'q */
ochir(id: number): Promise<boolean>;
}Muhim qaror — hamma metod Promise qaytaradi, garchi xotiradagi Map sinxron bo'lsa ham. Bazaga so'rov doim asinxron. Interfeys bugun sinxron bo'lsa, 26-qismda uni va uni chaqirgan har qatorni (await qo'shish uchun) o'zgartirish kerak bo'lardi. Bugun ozgina ortiqcha async — ertaga katta o'zgarishsiz almashuv.
Ikkinchi qaror — royxat filtrni parametr sifatida oladi ({ bajarildi }), barcha qatorni qaytarib xizmatda filtrlamaydi. Bazada filtr WHERE ga aylanadi — 10 000 qatorni Node'ga tortib, keyin tashlash shart emas.
4.2 XotiraOmbori
Holat 07 dagi src/ombor.ts dagi klass src/vazifalar/xotira-ombor.ts ga ko'chadi va interfeysni amalga oshiradi. Kanon fayli to'liq:
// xotira-ombor.ts — VazifaOmbori ning xotiradagi amalga oshirilishi
// (jarayon to'xtasa — ma'lumot yo'qoladi)
import { teglariniOl } from "./vazifa.ts";
import type {
Vazifa, VazifaOzgarishi, YangiVazifa,
} from "./vazifa.ts";
import type { RoyxatSorovi, VazifaOmbori } from "./ombor.ts";
export class XotiraOmbori implements VazifaOmbori {
readonly #vazifalar = new Map<number, Vazifa>();
#keyingiId = 1;
readonly #soat: () => Date;
constructor(soat: () => Date = () => new Date()) {
this.#soat = soat;
}
// structuredClone: chaqiruvchi qaytgan obyektni o'zgartirsa ham
// ombordagi nusxa buzilmaydi
royxat(sorov: RoyxatSorovi): Promise<Vazifa[]> {
let royxat = [...this.#vazifalar.values()];
if (sorov.bajarildi !== undefined) {
royxat = royxat.filter((v) => v.bajarildi === sorov.bajarildi);
}
return Promise.resolve(structuredClone(royxat));
}
ol(id: number): Promise<Vazifa | undefined> {
const v = this.#vazifalar.get(id);
return Promise.resolve(v && structuredClone(v));
}
yarat(yangi: YangiVazifa): Promise<Vazifa> {
const hozir = this.#soat().toISOString();
const v: Vazifa = {
id: this.#keyingiId++,
matn: yangi.matn,
bajarildi: yangi.bajarildi ?? false,
teglar: teglariniOl(yangi.matn),
yaratildi: hozir,
yangilandi: hozir,
};
this.#vazifalar.set(v.id, v);
return Promise.resolve(structuredClone(v));
}
yangila(id: number, ozgarish: VazifaOzgarishi):
Promise<Vazifa | undefined> {
const v = this.#vazifalar.get(id);
if (!v) return Promise.resolve(undefined);
if (ozgarish.matn !== undefined) {
v.matn = ozgarish.matn;
v.teglar = teglariniOl(ozgarish.matn);
}
if (ozgarish.bajarildi !== undefined) {
v.bajarildi = ozgarish.bajarildi;
}
v.yangilandi = this.#soat().toISOString();
return Promise.resolve(structuredClone(v));
}
ochir(id: number): Promise<boolean> {
return Promise.resolve(this.#vazifalar.delete(id));
}
}(Faqat format o'zgardi: qatorlar telefon uchun bo'lingan, bir qatorli if lar qavsga olingan.) Metodlar async emas, Promise.resolve(...) qaytaradi. Sabab — lint qoidasi (require-await): ichida await bo'lmagan async funksiya ogohlantirish beradi. Natija bir xil — Promise.
structuredClone (sayoz va chuqur nusxalash) — chuqur nusxa. Usiz marshrut qaytgan obyektni o'zgartirsa (masalan, javobdan oldin maydon qo'shsa), ombordagi asl obyekt ham o'zgarardi. Bazada bunday muammo yo'q — har so'rov yangi obyekt beradi. Xotira ombori ham shunday o'zini tutsin.
soat parametri — testlarda vaqtni boshqarish uchun (vaqt va tasodif darsida chuqur).
5. Xizmat va DI framework'siz
5.1 vazifaXizmati(ombor)
Kanon fayli to'liq:
// xizmat.ts — biznes qoidalari (service qatlami): HTTP'ni bilmaydi,
// omborni interfeys orqali oladi
import { HttpXato } from "../umumiy/http-xato.ts";
import type { VazifaOmbori } from "./ombor.ts";
import type {
Vazifa, VazifaOzgarishi, YangiVazifa,
} from "./vazifa.ts";
export const MAKS_VAZIFA = 1000;
// DI framework'siz: factory funksiya bog'liqlikni parametr sifatida
// oladi (testda — boshqa ombor)
export function vazifaXizmati(ombor: VazifaOmbori) {
async function topilsin(id: number): Promise<Vazifa> {
const v = await ombor.ol(id);
if (!v) throw new HttpXato(404, `Vazifa ${String(id)} topilmadi`);
return v;
}
return {
royxat: (bajarildi?: boolean) =>
ombor.royxat(bajarildi === undefined ? {} : { bajarildi }),
ol: topilsin,
async yarat(yangi: YangiVazifa): Promise<Vazifa> {
if ((await ombor.royxat({})).length >= MAKS_VAZIFA) {
throw new HttpXato(409,
`Ro'yxat to'lgan (${String(MAKS_VAZIFA)})`);
}
return ombor.yarat(yangi);
},
async yangila(id: number, ozgarish: VazifaOzgarishi):
Promise<Vazifa> {
const v = await ombor.yangila(id, ozgarish);
if (!v) {
throw new HttpXato(404, `Vazifa ${String(id)} topilmadi`);
}
return v;
},
async ochir(id: number): Promise<void> {
if (!(await ombor.ochir(id))) {
throw new HttpXato(404, `Vazifa ${String(id)} topilmadi`);
}
},
};
}
export type VazifaXizmati = ReturnType<typeof vazifaXizmati>;Yangi qoida paydo bo'ldi: 1000 tadan ko'p vazifa — 409 Conflict ("ro'yxat to'lgan"). Bu — biznes qoidasi, shuning uchun xizmatda. Halol eslatma: xizmat HttpXato tashlaydi, ya'ni HTTP status kodini biladi. Toza nazariyada xizmat o'z domen xatosini (VazifaTopilmadi) tashlaydi, marshrut uni statusga aylantiradi. Kanon buni xato turlari darsida tartibga soladi. Hozircha — soddalik uchun.
5.2 Dependency injection nima
Dependency injection (bog'liqlikni tashqaridan berish, DI) — obyekt o'ziga kerak narsani o'zi yaratmaydi, tashqaridan oladi. vazifaXizmati ichida new XotiraOmbori() yo'q — ombor parametr. Kim chaqirsa, o'sha qaysi omborni berishini hal qiladi:
ishga-tushir.ts—new XotiraOmbori()(26-qismda — Postgres ombori);- test — sekin, buzuq yoki to'la soxta ombor.
Buning uchun kutubxona shart emas — factory (yasovchi) funksiyalar yetadi: vazifaXizmati(ombor), vazifalarRouteri(xizmat), ilovaYarat({ ombor }). Bog'liqliklar bitta joyda — ilovaYarat da — bir-biriga ulanadi. Katta loyihalarda bu "ulash" ko'payib ketsa, DI konteynerlar ishlatiladi: awilix (Express uchun mashhur) yoki NestJS'ning o'rnatilgan DI'si. Kanon awilix'ni o'rnatmadi — uch factory uchun kerak emas.
Soxta ombor bilan xizmatni HTTP'siz sinash — saytda ham ishlaydi (xizmat mantiqining JS nusxasi):
function vazifaXizmati(ombor) {
return {
async yarat(yangi) {
if ((await ombor.royxat({})).length >= 1000) {
throw new Error("409: Ro'yxat to'lgan (1000)");
}
return ombor.yarat(yangi);
},
};
}
// Soxta ombor: 1000 ta vazifa bordek ko'rsatadi
const toliqOmbor = {
royxat: async () => new Array(1000).fill({}),
yarat: async () => ({ id: 1001 }),
};
const boshOmbor = {
royxat: async () => [],
yarat: async (y) => ({ id: 1, ...y }),
};
const xizmat = vazifaXizmati(boshOmbor);
console.log(await xizmat.yarat({ matn: "Non olish" }));
try {
await vazifaXizmati(toliqOmbor).yarat({ matn: "Choy" });
} catch (e) {
console.log(e.message);
}Konsolda:
{ id: 1, matn: 'Non olish' }
409: Ro'yxat to'lgan (1000)1000 ta haqiqiy vazifa yaratmasdan, server ochmasdan qoidani tekshirdik. Kanon mock, stub, spy va fake darsida xuddi shu usul bilan BuzuqOmbor yozadi — 500 xatosini sinash uchun.
Tekshirib ko'ring: Hamkasbingiz xizmat ichida
import { XotiraOmbori } from "./xotira-ombor.ts"qilib,const ombor = new XotiraOmbori()yozdi. Bu kod ishlaydi. Qanday muammo bor?
Javob
Xizmat endi aniq bitta omborga bog'langan. Testda soxta ombor berib bo'lmaydi. 26-qismda Postgres'ga o'tishda xizmat kodini o'zgartirish kerak. Ikki xizmat yaratilsa — ikki alohida xotira, ma'lumot ikkiga bo'linadi. DI bilan bularning hammasi chaqiruvchining qarori.
6. Marshrut (controller) va yakuniy ilova.ts
6.1 vazifalarRouteri(xizmat)
Kanon fayli to'liq (qatorlar bo'lingan):
// marshrut.ts — /vazifalar routeri va controller'lar:
// HTTP ↔ xizmat (req'dan o'qiydi, res'ga yozadi)
import { Router } from "express";
import type { RequestHandler } from "express";
import { HttpXato } from "../umumiy/http-xato.ts";
import { jsonKerak } from "../umumiy/json-kerak.ts";
import { ozgarishniTekshir, yangiVazifaniTekshir } from "./vazifa.ts";
import type { VazifaXizmati } from "./xizmat.ts";
function idniOl(xom: string): number {
if (!/^[1-9]\d{0,9}$/.test(xom)) {
throw new HttpXato(400, "id — musbat butun son");
}
return Number(xom);
}
// Express metod mos kelmasa 404 beradi (yo'l bor-yo'qligini
// bilmaydi) — 405 + Allow ni o'zimiz qo'yamiz
function metodYoq(allow: string): RequestHandler {
return (req, res) => {
res.set("Allow", allow);
throw new HttpXato(405, `${req.method} bu manzilda yo'q`);
};
}
export function vazifalarRouteri(xizmat: VazifaXizmati): Router {
const r = Router();
r.route("/")
// Express 5: async handler'dagi throw / rad etilgan Promise o'zi
// next(err) ga boradi
.get(async (req, res) => {
const b = req.query.bajarildi;
if (b !== undefined && b !== "true" && b !== "false") {
throw new HttpXato(400, "bajarildi — true yoki false");
}
res.json(await xizmat.royxat(
b === undefined ? undefined : b === "true"));
})
.post(jsonKerak, async (req, res) => {
const t = yangiVazifaniTekshir(req.body);
if (!t.ok) throw new HttpXato(422, t.xato);
const v = await xizmat.yarat(t.qiymat);
res.status(201)
.location(`${req.baseUrl}/${String(v.id)}`).json(v);
})
.all(metodYoq("GET, HEAD, POST"));
r.route("/:id")
.get(async (req, res) => {
res.json(await xizmat.ol(idniOl(req.params.id)));
})
.patch(jsonKerak, async (req, res) => {
const id = idniOl(req.params.id);
const t = ozgarishniTekshir(req.body);
if (!t.ok) throw new HttpXato(422, t.xato);
res.json(await xizmat.yangila(id, t.qiymat));
})
.delete(async (req, res) => {
await xizmat.ochir(idniOl(req.params.id));
res.sendStatus(204);
})
.all(metodYoq("GET, HEAD, PATCH, DELETE"));
return r;
}Oraliq ilova.ts dagi marshrutlar bilan solishtiring: idniOl, metodYoq, jsonKerak — o'sha. O'zgargani uchta: yo'llar nisbiy (/, /:id), ombor o'rnida xizmat (404 va 409 endi xizmatda), ishlovchilar async (xizmat Promise qaytaradi). Express 5 async dagi throw ni o'zi ushlaydi (middleware darsi).
6.2 Yakuniy ilova.ts
idniOl, metodYoq va marshrutlar ketdi. xatoIshlovchi o'zgarmaydi — u middleware darsida to'liq ko'rsatilgan (fayl boshida, ilovaYarat dan oldin turadi). Qolgani (kanon):
// ilova.ts — Express ilovasi: middleware tartibi → routerlar
// → 404 → xato ishlovchisi. listen() YO'Q — server
// ishga-tushir.ts da, testlar ilovani portsiz ham ishlata oladi
import express from "express";
import type { ErrorRequestHandler } from "express";
import { join } from "node:path";
import { HttpXato } from "./umumiy/http-xato.ts";
import { vazifaXizmati } from "./vazifalar/xizmat.ts";
import { vazifalarRouteri } from "./vazifalar/marshrut.ts";
import type { VazifaOmbori } from "./vazifalar/ombor.ts";
export type Bogliqliklar = { ombor: VazifaOmbori };
// const xatoIshlovchi: ErrorRequestHandler = … (o'zgarmagan)
export function ilovaYarat({ ombor }: Bogliqliklar): express.Express {
const app = express();
app.disable("x-powered-by"); // "Express" ekanini oshkor qilmaymiz
app.use(express.static(join(import.meta.dirname, "..", "public")));
app.use(express.json({ limit: "16kb" }));
app.use("/vazifalar", vazifalarRouteri(vazifaXizmati(ombor)));
app.use((req) => {
throw new HttpXato(404, `${req.method} ${req.path} topilmadi`);
});
app.use(xatoIshlovchi);
return app;
}Bitta qatorga qarang — vazifalarRouteri(vazifaXizmati(ombor)). Bu butun DI: ombor xizmatga, xizmat routerga beriladi. Bogliqliklar turi endi XotiraOmbori emas, VazifaOmbori interfeysi.
Endi bitta POST /vazifalar so'rovining yo'lini qatlamlar bo'ylab kuzatamiz:
| Qadam | Qayerda | Natija |
|---|---|---|
| 1 | express.static |
public/ da bunday fayl yo'q → next() |
| 2 | express.json |
tana o'qildi → req.body |
| 3 | router /vazifalar → .post(jsonKerak, …) |
Content-Type JSON emas → 415 |
| 4 | marshrut: yangiVazifaniTekshir |
tana noto'g'ri → 422 |
| 5 | xizmat: yarat |
1000 ta bor → 409 |
| 6 | ombor: XotiraOmbori.yarat |
id berdi, nusxa qaytardi |
| 7 | marshrut: res.status(201).location(…).json(v) |
201 — zanjir to'xtadi |
3–5-qadamlardagi har throw 404 middleware'ini chetlab, to'g'ri xatoIshlovchi ga boradi (middleware darsi). Har qatlam faqat o'z ishini qiladi: tekshiruv — marshrutda, qoida — xizmatda, saqlash — omborda.
7. Hujumchi nigohi
Tuzilma o'zi hujumni to'xtatmaydi, lekin xatolarni kamaytiradi:
| Xavf | Tartibsiz kodda | Qatlamli kodda |
|---|---|---|
| Qoida bir joyda tekshirildi, boshqasida unutildi (1000 limiti faqat POST'da, import'da yo'q) | ko'p nusxa | qoida bitta — xizmatda |
| Validatsiya o'tkazib yuborildi | har marshrutda o'zi | marshrut — chegara: idniOl, yangiVazifaniTekshir |
| Bazaga xom satr (SQL injection) | marshrut ichida SQL | SQL faqat omborda, parametrli (26-qism) |
| Ichki xato mijozga | har joyda res.json(e) |
bitta xato ishlovchi |
Birinchi qator haqida: kanon bulk operatsiyalar darsida butun ro'yxatni almashtirish (PUT /v1/vazifalar) qo'shadi. Agar 1000 limiti marshrutda bo'lganida, uni yangi marshrutda unutish oson edi. Xizmatda — avtomatik.
8. API qadami: Router va qatlamlar (holat 13, commit)
Holat 13 ning oxirgi bo'lagi. Bugun commit.
src/vazifalar/papkasini yarating va ko'chiring:
git mv src/vazifa.ts src/vazifalar/vazifa.ts
git mv src/ombor.ts src/vazifalar/xotira-ombor.tssrc/vazifalar/ombor.ts— «VazifaOmboriinterfeysi» bo'limidagi fayl.src/vazifalar/xotira-ombor.ts— «XotiraOmbori» bo'limidagi fayl (implements VazifaOmbori,Promise,royxat(sorov)).src/vazifalar/xizmat.tsvasrc/vazifalar/marshrut.ts— yuqoridagi to'liq fayllar.src/ilova.ts— «Yakuniyilova.ts» bo'limidagidek.src/ishga-tushir.ts— bitta import:
-import { XotiraOmbori } from "./ombor.ts";
+import { XotiraOmbori } from "./vazifalar/xotira-ombor.ts";test/api.test.ts— bitta import:
-import { teglariniOl } from "../src/vazifa.ts";
+import { teglariniOl } from "../src/vazifalar/vazifa.ts";Testlarning boshqa joyi o'zgarmaydi — API tashqaridan bir xil. Bu refaktoringning (tashqi xulqni o'zgartirmasdan ichki tuzilmani yaxshilash) eng yaxshi tekshiruvi.
Kanon tuzilmasi (holat 13):
src/server.ts kirish: ishgaTushir + signallar
src/ishga-tushir.ts createServer(ilovaYarat(...)) + timeout'lar
src/ilova.ts static → json → /vazifalar → 404 → xatolar
src/yopish.ts graceful shutdown
src/umumiy/http-xato.ts HttpXato(holat, xabar)
src/umumiy/json-kerak.ts marshrut darajasidagi middleware (415)
src/vazifalar/vazifa.ts model, teglar, qo'lda tekshiruv
src/vazifalar/ombor.ts interface VazifaOmbori
src/vazifalar/xotira-ombor.ts class XotiraOmbori
src/vazifalar/xizmat.ts vazifaXizmati(ombor)
src/vazifalar/marshrut.ts vazifalarRouteri(xizmat)Holat 13 tekshiruvi (kanon, Node 24.21.0): tip 0, lint 0, testlar 7/7. Prod bog'liqlik — 1 (express@5.3.0, daraxtda 65 paket); node_modules — 171 paket, 43.2 MB. Sovuq start 289 ms. Yuk (autocannon, 10 ulanish × 5 s, band mashina, bitta o'lchov): GET /vazifalar 3 237 req/s, GET /vazifalar/5 9 724 req/s — Express'ga kirish darsidagi raqamlar aynan shu holatniki.
npm run check
git add .
git commit -m "Express 5 ga o'tish: Router, qatlamlar (marshrut → xizmat → ombor), VazifaOmbori interfeysi"9. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| Routerdagi marshrut 404 | router ichida to'liq yo'l yozilgan | nisbiy yo'l (/, /:id) |
req.params.id — undefined ichki routerda |
mergeParams yo'q |
Router({ mergeParams: true }) |
Location eski yo'lni ko'rsatadi |
yo'l qattiq yozilgan | req.baseUrl |
| Testda soxta ombor berib bo'lmaydi | xizmat new XotiraOmbori() ni o'zi qiladi |
DI: parametr orqali |
| Qaytgan obyektni o'zgartirish omborni buzdi | nusxa emas, havola | structuredClone |
Property 'royxat' is missing in type … |
klass interfeysni to'liq amalga oshirmagan | hamma metod, to'g'ri imzo |
Oxirgisi — TypeScript xatosi (TS2420): "XotiraOmbori klassi VazifaOmbori interfeysini noto'g'ri amalga oshiryapti — royxat xususiyati yo'q". implements ning foydasi shu: interfeys o'zgarsa, kompilyator hamma amalga oshirishlarni ko'rsatadi.
10. Mashqlar
Yechimlar kurs/mashqlar/25/13-router/ da.
1-mashq (oson): to'ldiring
- Router ota yo'ldagi parametrlarni ko'rishi uchun —
Router({ : true }) - HTTP'ni biladigan qatlam — (controller)
- Biznes qoidalari qatlami — (service)
- Saqlashni yashiradigan qatlam — (ombor)
- Bog'liqlikni ichkarida yaratmay, parametr orqali olish — dependency
Yechim
- mergeParams — usiz
req.paramsbo'sh. - marshrut —
req→ xizmat →res. - xizmat — 404, 409 "ro'yxat to'lgan".
- repository —
VazifaOmboriinterfeysi. - injection —
vazifaXizmati(ombor).
2-mashq (o'rta): qoida xizmatda
vazifaXizmati ning JS nusxasiga («DI nima» bo'limidagi) yangi qoida qo'shing: bir xil matnli vazifa ikkinchi marta yaratilmasin — Error("409: bunday vazifa bor"). Matn solishtirish katta-kichik harfga qaramasin. Soxta ombor bilan sinang: ombordagi "Non olish", yangi — "non olish" (rad), "Choy" (qabul). Eslatma: some — massivda qidirish.
Yechim
function vazifaXizmati(ombor) {
return {
async yarat(yangi) {
const royxat = await ombor.royxat({});
const kichik = yangi.matn.toLowerCase();
if (royxat.some((v) => v.matn.toLowerCase() === kichik)) {
throw new Error("409: bunday vazifa bor");
}
return ombor.yarat(yangi);
},
};
}
const ombor = {
vazifalar: [{ id: 1, matn: "Non olish" }],
async royxat() {
return this.vazifalar;
},
async yarat(yangi) {
const v = { id: this.vazifalar.length + 1, ...yangi };
this.vazifalar.push(v);
return v;
},
};
const xizmat = vazifaXizmati(ombor);
for (const matn of ["non olish", "Choy"]) {
try {
console.log(await xizmat.yarat({ matn }));
} catch (e) {
console.log(e.message);
}
}Konsolda:
409: bunday vazifa bor
{ id: 2, matn: 'Choy' }Qoida xizmatda — marshrut ham, ombor ham o'zgarmadi. Postgres'da bu qoida yana bir himoya oladi: UNIQUE cheklovi (26-qism). Biri — tushunarli xabar uchun, ikkinchisi — parallel so'rovlar uchun.
3-mashq (qiyin): ichma-ich router
Express 5.3.0 bilan izohlarRouteri(ombor) yozing: /vazifalar/:id/izohlar ga ulanadi, GET — shu vazifaning izohlari (yo'q bo'lsa []), POST — req.body.matn ni qo'shadi, 201 va Location: <req.baseUrl>. Ombor — Map (vazifa id → izohlar massivi), DI bilan beriladi. 7-vazifaga izoh qo'shib, 7 va 8 ning ro'yxatini chiqaring.
Yechim
import express from "express";
// ombor — oddiy Map: vazifa id → izohlar massivi
function izohlarRouteri(ombor) {
const r = express.Router({ mergeParams: true });
r.get("/", (req, res) => {
res.json(ombor.get(req.params.id) ?? []);
});
r.post("/", (req, res) => {
const royxat = ombor.get(req.params.id) ?? [];
royxat.push(req.body.matn);
ombor.set(req.params.id, royxat);
res.status(201).location(req.baseUrl).json(royxat);
});
return r;
}
const app = express();
app.use(express.json());
app.use("/vazifalar/:id/izohlar", izohlarRouteri(new Map()));
const server = app.listen(0, "127.0.0.1");
await new Promise((ok) => server.once("listening", ok));
const url = `http://127.0.0.1:${server.address().port}`
+ "/vazifalar/7/izohlar";
const r1 = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ matn: "Non issiq bo'lsin" }),
});
console.log(r1.status, r1.headers.get("location"), await r1.text());
console.log(await (await fetch(url)).text());
const boshqa = url.replace("/7/", "/8/");
console.log(await (await fetch(boshqa)).text());
server.closeAllConnections();
server.close();Konsolda:
201 /vazifalar/7/izohlar ["Non issiq bo'lsin"]
["Non issiq bo'lsin"]
[]mergeParams siz req.params.id — undefined, hamma izoh bitta undefined kalitiga tushardi va 8-vazifa ham 7 ning izohini ko'rardi. Haqiqiy loyihada izohlar xizmati ham bo'ladi (vazifa bormi — 404) — bu yerda qisqalik uchun yo'q.
4-mashq: API qadami
«API qadami» bo'limidagi 1–7 qadamlarni bajaring, npm run check — 7/7, keyin commit. Qo'shimcha: test/ ga xizmatning birlik testini yozing — soxta ombor (royxat 1000 elementli massiv qaytaradi) bilan yarat 409 li HttpXato tashlashini assert.rejects bilan tekshiring. Server kerak emas.
Yechim
Fayllar — darsda to'liq. Birlik testi g'oyasi (kanonga kirmagan, o'quvchi mashqi):
test("xizmat: 1000 ta vazifadan keyin 409", async () => {
const toliq = new XotiraOmbori();
for (let i = 0; i < 1000; i++) {
await toliq.yarat({ matn: `v${String(i)}` });
}
const yangi = { matn: "ortiqcha" };
await assert.rejects(vazifaXizmati(toliq).yarat(yangi),
(x: unknown) => x instanceof HttpXato && x.holat === 409);
});Soxta obyekt o'rniga haqiqiy XotiraOmbori ham ishlaydi — u tez. assert.rejects ning ikkinchi argumenti — xatoni tekshiruvchi funksiya. String(i) — lint qoidasi (restrict-template-expressions) shablonda sonni to'g'ridan qabul qilmaydi. Biz tekshirdik: tip 0, lint 0, npm test — 8/8.
11. Real ishda
- Har o'rta va katta Express loyihasi —
routes/,services/,repositories/yoki feature-folder; intervyuda "loyihangiz tuzilmasini tushuntiring" deyarli doim so'raladi. - NestJS (30-qism) — xuddi shu qatlamlar majburiy: Controller, Service (Provider), Repository; DI o'rnatilgan.
- Bazani almashtirish — repository naqshining asosiy foydasi; 26-qismda
vazifalar-apibuni ko'rsatadi. - Testlar — xizmat qatlami HTTP'siz, soxta ombor bilan; marshrutlar — integratsion test bilan (Supertest darsi).
Xulosa
express.Router()— alohida ulanadigan mini-ilova; ichida yo'llar nisbiy,req.baseUrl— ulangan yo'l; ota parametrlari uchunmergeParams: true.- Feature-folder (
vazifalar/) + qatlamlar: marshrut (HTTP) → xizmat (qoidalar) → ombor (saqlash); o'qlar faqat pastga. VazifaOmbori— interfeys, hamma metodPromise: 26-qismda Postgres ombori marshrut va xizmatni o'zgartirmasdan qo'shiladi.- DI framework'siz: factory funksiyalar (
vazifaXizmati(ombor),vazifalarRouteri(xizmat)); testda soxta ombor. - Holat 13: Express 5.3.0, 65 paket, testlar 7/7 — commit.
Keyingi dars: Mashhur middleware'lar — helmet, CORS, compression, morgan, rate limit: qaysi biri vazifalar-api ga kirdi va qaysi biri o'lchovdan o'tmadi.
Manbalar
- Express 5.x API —
express.Router([options]),mergeParams,req.baseUrl: https://expressjs.com/en/5x/api.html#router - Express — "Routing" (
express.Router): https://expressjs.com/en/guide/routing.html (2026-10-24 da ochildi) - awilix — README (DI konteyner): https://github.com/jeffijoe/awilix (o'rnatilmadi)
- Kanon:
vazifalar-apiholat 13 (ilova.ts,vazifalar/*,umumiy/*, testlar, tekshiruv: tip/lint 0, 7/7, 65 paket, sovuq start 289 ms, yuk).mergeParams, DI va izohlar routeri misollari — biz ishga tushirdik (Express 5.3.0, Node 24.21.0, Windows 11).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!