IlmHamroh
JavaScript Full-stack/30-qism. NestJS6/65-dars29 daqiqa
Mundarija (37)

NestJS bootstrap: main.ts, NestFactory.create va ilova sozlamalari

Qisqacha: Bootstrap — ilovani ishga tushirish: main.ts da NestFactory.create(AppModule, sozlamalar) modul daraxtini yig'adi, provider'larni yaratadi va marshrutlarni Express'ga ulaydi; app.listen(port, host) esa portni ochadi. Ikkalasi orasida global sozlamalar beriladi: setGlobalPrefix, enableCors, enableVersioning, useGlobalFilters, Express middleware'lari (app.use). Yaratish sozlamalari (rawBody, bufferLogs, bodyParser, logger) esa create ning ikkinchi argumentida. Bu darsda vazifalar-api to'liq Nest'ga o'tadi.

Bu darsda

  • NestFactory.create nima qilishini va uning asosiy sozlamalarini bilasiz.
  • setGlobalPrefix, enableCors, rawBody, bufferLogs, listen(port, host) ni haqiqiy chiqish bilan ko'rasiz.
  • Express middleware'larini Nest ilovasiga tartib bilan ulaysiz va versiyalashni sozlaysiz.
  • Nest Express xatolarini filtrdan oldin "o'zgartirib yuborishini" va uni qanday tuzatishni bilasiz.
  • vazifalar-api ning eski Express fayllarini o'chirasiz, testlarni Vitest'ga o'tkazasiz va commit qilasiz.

Oldin bilishingiz kerak: Provider'lar va service'lar, Controller'lar va route dekoratorlari, Middleware va request lifecycle, Global error handler, API versiyalash va deprecation, Reverse proxy orqasida Node.

O'tgan darsda xizmatlar yozildi, lekin test Symbol(VAZIFA_OMBORI) topilmadi deb yiqildi: BogliqliklarModule daraxtga ulanmagan edi. Bu darsda ilovani yig'adigan joy — main.ts va ilova.ts — Nest'ga o'tadi. Dars oxirida Express routerlari o'chadi va 25–27 dagi testlar Nest ilovasini tekshiradi.

1. Nega bu kerak?

25-qismda ishga tushirish ikki faylda edi. ilova.ts Express ilovasini yig'ardi: middleware'lar, routerlar, 404, xato ishlovchi. server.ts esa config, ombor va serverni ulardi. Tartib qo'lda, har qadam ko'rinib turardi.

Nest'da marshrutlar va xizmatlar endi modul ichida. Lekin ularni kimdir ishga tushirishi kerak: modul daraxtini yig'ish, nusxalarni yaratish, marshrutlarni HTTP serverga ulash. Bu ish — bootstrap. Undan tashqari, ilovaning hammasiga tegishli sozlamalar bor: CORS, yo'l prefiksi, versiya, xato formati. Ular bitta modulga emas, butun ilovaga tegishli — shuning uchun bootstrap'da beriladi.

O'xshatish: to'yxona. Modullar — oshxona, zal, musiqa. Har biri o'z ishini biladi. Lekin eshikni kim ochadi, mehmonni qaysi eshikdan kiritadi, qaysi soatda boshlanadi — bu boshqaruvchining ishi. main.ts — o'sha boshqaruvchi.

2. NestFactory.create

2.1 Nima qiladi

ts
const app = await NestFactory.create<NestExpressApplication>(
  AppModule, { bodyParser: false, logger: ["error", "warn"] },
);

Bitta chaqiruv ichida to'rt ish bo'ladi:

  1. AppModule dan boshlab imports daraxti yig'iladi (Modullar).
  2. Har provider uchun bog'liqliklar topiladi va nusxalar yaratiladi (Provider'lar).
  3. Controller metodlari Express marshrutlariga aylanadi (logdagi Mapped {…} qatorlari).
  4. Platform adapter (sukut — Express) tayyorlanadi.
flowchart TB
  M["main.ts: config, omborYarat"] --> C["NestFactory.create"]
  C --> D["modul daraxti, provider nusxalari"]
  D --> G["global sozlamalar: app.use, versiya, filtr"]
  G --> I["app.init: marshrutlar ulanadi"]
  I --> L["app.listen(port, host)"]
  L --> J["jarayonniBoshqar: signal, yopish"]

Diagrammani yuqoridan o'qing: create ichida modullar va nusxalar paydo bo'ladi, global sozlamalar undan keyin beriladi, port esa eng oxirida ochiladi. Testlar listen gacha bo'lgan qismni ishlatadi.

Natija — ilova obyekti, lekin port hali ochilmagan. create dan keyin, listen dan oldin global sozlamalar beriladi. <NestExpressApplication> generigi — Express'ga xos metodlar uchun: app.set("trust proxy", …), app.disable("x-powered-by"), app.useStaticAssets(…). Usiz ular turda ko'rinmaydi.

2.2 Yaratish sozlamalari

Ikkinchi argument — faqat yaratish paytida ma'noli sozlamalar:

Sozlama Nima qiladi vazifalar-api da
logger Nest'ning o'z logi: false, darajalar ro'yxati yoki logger obyekti ["error", "warn"] (testda false)
bodyParser Nest o'z JSON/urlencoded parser'ini qo'yadimi false — o'zimizniki
rawBody req.rawBody da xom bayt (Buffer) saqlansinmi yo'q
bufferLogs useLogger chaqirilguncha loglar kutsinmi yo'q
cors enableCors ning qisqa shakli yo'q (cors paketi)
abortOnError yaratishda xato bo'lsa jarayonni o'ldirsinmi Lifecycle hook'lar darsida

bodyParser: false ning sababi: 25-qismda JSON parser'ni o'zimiz sozlagandik — 16 KB limit va application/merge-patch+json turi (Request body o'qish va hajm cheklovi). Nest'ning o'z parser'i bunday sozlanmagan. Ikkita parser yonma-yon bo'lsa, birinchisi tanani o'qib oladi va bizning limit ishlamay qoladi. Shuning uchun Nest'nikini o'chiramiz va o'zimiznikini app.use(express.json(…)) bilan qo'yamiz.

3. Global sozlamalar — haqiqiy tajriba

3.1 Tajriba sharti

Xaritadagi bootstrap sozlamalarini bitta demo ilovada sinab ko'rdik (biz ishga tushirdik: NestJS 12.2.0, Node 24.21.0). Ikki controller — vazifalar (ichida xom tana ko'rsatadigan webhook) va salomatlik:

ts
@Controller('vazifalar')
class VazifalarController {
  @Get() royxat() {
    return [];
  }
  @Post('webhook') webhook(@Req() req: RawBodyRequest<Request>) {
    return {
      rawBody: req.rawBody?.constructor.name,
      uzunlik: req.rawBody?.length,
      body: req.body as unknown,
    };
  }
}

const app = await NestFactory.create(AppModule, {
  rawBody: true,
  bufferLogs: true,
});
app.useLogger(new MeningLogim()); // har qatorga [mening] yorlig'i
app.setGlobalPrefix('api', { exclude: ['salomatlik'] });
app.enableCors({ origin: ['http://localhost:5173'] });
await app.listen(0, '0.0.0.0');

3.2 bufferLogs va useLogger

Ishga tushish logi:

text
[mening] NestFactory: Starting Nest application...
[mening] InstanceLoader: AppModule dependencies initialized
[mening] RoutesResolver: VazifalarController {/api/vazifalar}:
[mening] RouterExplorer: Mapped {/api/vazifalar, GET} route
[mening] RouterExplorer: Mapped {/api/vazifalar/webhook, POST} route
[mening] RoutesResolver: SalomatlikController {/api/salomatlik}:
[mening] RouterExplorer: Mapped {/salomatlik, GET} route
[mening] NestApplication: Nest application successfully started

Birinchi qatorga qarang: Starting Nest application... ham bizning logger orqali chiqdi. Holbuki useLogger create dan keyin chaqirilgan. bufferLogs: true shuni qiladi: create paytidagi loglar xotirada kutadi va useLogger dan keyin yangi logger'ga beriladi. Usiz birinchi qatorlar Nest'ning sukut formatida chiqardi, keyingilari — sizning formatingizda. JSON log yig'adigan tizim uchun bu aralash qatorlar muammo (Pino chuqur). Nest logini pino'ga ulash — NestJS Logger va nestjs-pino darsida.

3.3 setGlobalPrefix va exclude

Logdagi Mapped qatorlarida /api prefiksi bor, salomatlik dan tashqari. So'rovlar:

text
GET /vazifalar → 404 (CORS: http://localhost:5173)
  <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET /vazifalar</pre>
</body>
</html>

GET /api/vazifalar → 200 (CORS: http://localhost:5173)
  []
GET /salomatlik → 200 (CORS: http://localhost:5173)
  {"ok":true}

setGlobalPrefix('api') hamma marshrut oldiga /api qo'shdi. exclude: ['salomatlik'] — istisno: salomatlik tekshiruvi prefikssiz qoldi. Orkestrator (Docker, Kubernetes) uni odatda ildiz yo'lda qidiradi.

Ikki kutilmagan narsa bor. Birinchisi — logdagi SalomatlikController {/api/salomatlik}: controller qatori prefiksni ko'rsatdi, haqiqiy marshrut esa /salomatlik. Ishonchli manba — Mapped qatorlari. Ikkinchisi muhimroq: GET /vazifalar ga javob HTML. Bu Nest'ning JSON 404 i emas, Express'ning sukut sahifasi. Nest o'z "topilmadi" ishlovchisini ham prefiks ostiga qo'yadi. Prefiksdan tashqaridagi so'rov Nest'ga umuman yetmaydi — uni Express o'zi yopadi. API mijozi JSON kutsa, bu sahifa uni chalg'itadi.

3.4 enableCors

Hamma javobda CORS: http://localhost:5173 — Access-Control-Allow-Origin sarlavhasi. app.enableCors(...) ichida aynan cors paketi ishlaydi (CORS chuqur). Sozlamalar ham bir xil: origin, allowedHeaders, exposedHeaders, maxAge. Shuning uchun vazifalar-api da 25-qismdagi app.use(cors({...})) qatori o'zgarmay qoladi — natija bir xil, kod esa tanish.

3.5 rawBody: true

text
POST /api/vazifalar/webhook → 201 (CORS: http://localhost:5173)
  {"rawBody":"Buffer","uzunlik":8,"body":{"id":7}}

req.body — tahlil qilingan JSON, req.rawBody — o'sha tananing xom baytlari (Buffer, 8 bayt: {"id":7}). Nega ikkalasi kerak? Kiruvchi webhook imzosi xom baytlar ustida hisoblanadi (Kiruvchi webhook'larni qabul qilish). Agar JSON.parse dan keyin qayta JSON.stringify qilsangiz, bo'shliqlar va kalitlar tartibi o'zgarishi mumkin — imzo mos kelmaydi. rawBody faqat Nest'ning o'z parser'i bilan ishlaydi; bodyParser: false bo'lsa, xom tanani o'zingiz saqlaysiz.

3.6 listen(port, host)

text
getUrl: http://127.0.0.1:<port>

app.listen(0, '0.0.0.0') — 0 port: "bo'sh portni o'zing tanla" (testlarda qulay). '0.0.0.0' — barcha tarmoq interfeyslarida tinglash. getUrl() baribir 127.0.0.1 ko'rsatdi — bu faqat ko'rsatish uchun manzil.

'0.0.0.0' va '127.0.0.1' farqi katta. Birinchisida port tashqi tarmoqdan ham ochiq. Ikkinchisida faqat shu kompyuter ichidan. Qoida Reverse proxy orqasida Node darsidan: proxy orqasidagi Node porti tashqariga ochilmasin — 127.0.0.1. Bizning API sukut bo'yicha shunday (HOST sozlamasi). 0.0.0.0 konteyner ichida kerak bo'ladi — u yerda tashqi dunyo bilan konteyner tarmog'i ajratib turadi (Docker — 33-qismda).

Tekshirib ko'ring: setGlobalPrefix('api') dan keyin mijoz eski /vazifalar yo'liga so'rov yubordi. U qanday javob oladi va nega bu yomon?

Javob

404 va HTML sahifa (Cannot GET /vazifalar). Nest'ning JSON xato ishlovchisi prefiks ostida — prefiksdan tashqaridagi so'rov unga yetmaydi, Express'ning sukut javobi chiqadi. JSON kutgan mijoz JSON.parse da yiqiladi, Problem Details formati ham yo'q. Shuning uchun vazifalar-api eski yo'llar uchun prefiks emas, versiyalash ishlatadi (keyingi bo'lim).

3.7 enableShutdownHooks

2-darsdagi skelet main.ts da app.enableShutdownHooks() bor edi. U Nest'ga SIGTERM va SIGINT signallarini tinglashni buyuradi: signal kelsa, Nest app.close() ni o'zi chaqiradi va modullarning yopish hook'lari ishlaydi.

vazifalar-api da bu qator bu darsdan boshlab yo'q. 25-qismda signal va drenaj uchun o'z kodimiz bor — jarayon.ts va yop() (Process darajasidagi xatolar va crash strategiyasi). Ikkalasi bir vaqtda signal tinglasa, yopish ikki marta boshlanadi. Nest'ning yopishi bajarilayotgan so'rov bor paytda qanday ishlashini kanon o'lchagan — natijasi va qarorning to'liq sababi Lifecycle hook'lar darsida.

4. Express middleware'lari Nest ilovasida

4.1 app.use — tartib saqlanadi

Nest ilovasi ichida Express ishlaydi. Shuning uchun 25-qismdagi middleware'lar o'zgarmasdan ulanadi:

ts
app.use(sorovId());
app.use(sorovLogi(log));
app.use(helmet({ strictTransportSecurity: hsts }));
app.use(cors({ /* … */ }));
app.useStaticAssets(join(import.meta.dirname, "..", "public"));
app.use(express.json({ limit: "16kb", type: JSON_TURLARI }));
app.use(tanaXatosi);
app.use(eskirgan(ESKI_YOLLAR));

app.use bilan qo'yilgan middleware Nest marshrutlaridan oldin ishlaydi va yozilgan tartibda. Tartib 25-qismdagidek: so'rov ID'si → log va AsyncLocalStorage konteksti → xavfsizlik sarlavhalari → CORS → statik fayllar → JSON tana (Middleware va request lifecycle). useStaticAssets — Express'dagi express.static ning Nest shakli. Nest'ning o'z middleware tizimi ham bor — Middleware Nest'da darsida.

4.2 Versiyalash: bitta controller, ikki yo'l

25-qismda v1 router ikki joyga ulangan edi: /v1 va eskirgan versiyasiz yo'l. Nest'da bu bitta sozlama:

ts
app.enableVersioning({
  type: VersioningType.URI,
  defaultVersion: ["1", VERSION_NEUTRAL],
});
  • type: VersioningType.URI — versiya yo'lning o'zida: /v1/vazifalar. Boshqa turlar ham bor: sarlavha (HEADER), Accept turi (MEDIA_TYPE), o'zingizning funksiyangiz (CUSTOM).
  • defaultVersion: ["1", VERSION_NEUTRAL] — versiyasi ko'rsatilmagan controller ikki versiyaga tegishli: 1 va "versiyasiz". Natijada @Controller("vazifalar") ham /v1/vazifalar da, ham /vazifalar da javob beradi.

Eskirgan yo'lga Deprecation va Sunset sarlavhalari — eskirgan(ESKI_YOLLAR) middleware'i, 25-qismdagidek. Kelajakda v2 paydo bo'lsa, yangi controller @Controller({ path: "vazifalar", version: "2" }) deb yoziladi.

Nega setGlobalPrefix("v1") emas? Prefiks hamma yo'lni almashtiradi — eski /vazifalar yo'qoladi va mijozlar Express'ning HTML 404 ini oladi (yuqoridagi tajriba). Versiyalash esa ikkala yo'lni bitta controller'ga yo'naltiradi.

5. Xato filtri va Nest'ning "tarjimasi"

5.1 Global filter

25-qismdagi xatoIshlovchi — oxirgi, 4 argumentli Express middleware edi. Nest'da uning o'rni — exception filter (xato filtri): @Catch() dekoratorli klass, uni app.useGlobalFilters(new ProblemDetailsFilter(…)) bilan ulaymiz. U hamma xatoni bir joyda Problem Details'ga aylantiradi (Xato javoblari: Problem Details). Filtrlar haqida to'liq — Exception filter'lar darsida. Bu darsda kanon filtrini o'qiymiz va bitta tuzoqni ko'ramiz.

Filtr Nest'ning o'z 404 ini ham ushlaydi. NestJS nima va nega darsida sukut javob {"message":"Cannot GET /yoq","error":"Not Found","statusCode":404} edi. Filtr bilan:

text
$ curl /yoq
HTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8
{"type":"about:blank","title":"Not Found","status":404,"detail":"GET /yoq — bunday manzil yo'q","instance":"/yoq",…}

5.2 Tuzoq: Nest xatoni filtrdan oldin o'zgartiradi

Kanon ko'chish paytida g'alati narsani topdi. Yaroqsiz JSON yuborilganda 25-qismda javob "detail":"Tana yaroqli JSON emas" edi. Nest'da esa bu xabar chiqmadi.

Sabab: body-parser xatoni http-errors obyekti sifatida beradi — unda status, expose va type: "entity.parse.failed" maydonlari bor. Nest esa Express xatosini filtrga uzatishdan oldin "xaritalaydi": SyntaxError ni BadRequestException(err.message) ga aylantiradi. type maydoni yo'qoladi. Filtr endi "bu JSON xatosi" ekanini bilmaydi.

Biz buni o'zimiz tekshirdik: tanaXatosi qatorini vaqtincha o'chirib, ikki so'rov yubordik (biz ishga tushirdik, SAQLASH=xotira). Yaroqsiz JSON ({"matn":):

text
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8
{"type":"about:blank","title":"Bad Request","status":400,"detail":"Unexpected end of JSON input","instance":"/v1/vazifalar",…}

20 000 belgili matn (16 KB dan katta):

text
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json; charset=utf-8
{"type":"about:blank","title":"Internal Server Error","status":500,"detail":"Kutilmagan xato. sorovId bilan murojaat qiling.",…}

Birinchisida parser'ning inglizcha ichki xabari mijozga chiqdi. Ikkinchisi yomonroq: 413 bo'lishi kerak edi, 500 chiqdi. Katta tana xatosi SyntaxError emas — Nest uni xaritalamadi, u HttpException ham emas — filtr uni "kutilmagan xato" deb oldi. Mijoz "server buzildi" deb o'ylaydi, aslida u o'zi katta tana yubordi.

flowchart LR
  P["express.json: xato"] --> T{"tanaXatosi bormi?"}
  T -- yo'q --> N["Nest: SyntaxError → 400, qolgani o'zgarmaydi"]
  N --> F1["filtr: inglizcha xabar yoki 500"]
  T -- ha --> X["IlovaXatosi(status, o'zbekcha)"]
  X --> F2["filtr: 400 / 413 aniq"]

Diagrammadagi ikki yo'lni solishtiring: pastki yo'lda xato Nest'ga yetganda allaqachon bizning turimizda.

Yechim — Nest'gacha ushlash. tanaXatosi — express.json dan keyin turadigan 4 argumentli Express middleware. U http-errors ni IlovaXatosi ga aylantiradi, type ma'lumoti yo'qolishidan oldin. Shu qator bilan:

text
HTTP/1.1 400 Bad Request
{"type":"about:blank","title":"Bad Request","status":400,"detail":"Tana yaroqli JSON emas",…}

HTTP/1.1 413 Payload Too Large
{"type":"about:blank","title":"Payload Too Large","status":413,"detail":"Tana 16 KB dan katta",…}

5.3 JS modelida xato yo'li

Xato qaysi yo'ldan o'tishini kichik modelda ko'raylik. Bu blok brauzerda ishlaydi. nestXaritasi — Nest'ning SyntaxError ni o'zgartiradigan qadami (soddalashtirilgan), filtr — bizning filtrning qisqa shakli:

js
const TANA = {
  "entity.parse.failed": "Tana yaroqli JSON emas",
  "entity.too.large": "Tana 16 KB dan katta",
};
function httpXato(Klass, status, type, xabar) {
  const e = new Klass(xabar);
  Object.assign(e, { status, type, expose: true });
  return e;
}
class IlovaXatosi extends Error {
  constructor(holat, xabar) { super(xabar); this.holat = holat; }
}
class BadRequest extends Error { holat = 400; }

const nestXaritasi = (x) =>
  x instanceof SyntaxError ? new BadRequest(x.message) : x;
const filtr = (x) =>
  x instanceof IlovaXatosi || x instanceof BadRequest
    ? `${x.holat} ${x.message}`
    : "500 Kutilmagan xato";
const tanaXatosi = (x) =>
  typeof x.status === "number" && x.expose
    ? new IlovaXatosi(x.status, TANA[x.type] ?? x.message)
    : x;

const xatolar = [
  httpXato(SyntaxError, 400, "entity.parse.failed",
    "Unexpected end of JSON input"),
  httpXato(Error, 413, "entity.too.large",
    "request entity too large"),
];
for (const x of xatolar) {
  console.log("tanaXatosi'siz: ", filtr(nestXaritasi(x)));
  const tuzatilgan = nestXaritasi(tanaXatosi(x));
  console.log("tanaXatosi bilan:", filtr(tuzatilgan));
}

Konsolda:

text
tanaXatosi'siz:  400 Unexpected end of JSON input
tanaXatosi bilan: 400 Tana yaroqli JSON emas
tanaXatosi'siz:  500 Kutilmagan xato
tanaXatosi bilan: 413 Tana 16 KB dan katta

To'rt qator — yuqoridagi to'rt haqiqiy javobning qisqa shakli. Model nimani soddalashtirgan: haqiqiy Nest BadRequestException ni @nestjs/common dan oladi va filtr Problem Details tanasini yasaydi. G'oya esa aynan: xato Nest'ga yetmasdan oldin tarjima qilinsa, ma'lumot yo'qolmaydi.

Tekshirib ko'ring: Nega tanaXatosi ni app.useGlobalFilters ga yangi filtr qilib qo'shish yechim emas?

Javob

Filtr Nest xaritalagandan keyin ishlaydi — u allaqachon BadRequestException ni yoki type siz xatoni oladi. Ma'lumot filtrgacha yo'qoladi. Shuning uchun tarjima Express darajasida, express.json dan darhol keyin, xato Nest'ga uzatilishidan oldin bo'lishi kerak.

6. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
16 KB limit ishlamaydi Nest parser'i ham yoqiq bodyParser: false + o'z express.json
Prefiksdan tashqari yo'lga HTML 404 setGlobalPrefix — Nest 404 i ham prefiks ostida exclude yoki versiyalash
Katta tana — 500 http-errors Nest'ga xom yetdi tanaXatosi kabi tarjima middleware'i
app.set/useStaticAssets turda yo'q generik berilmagan create<NestExpressApplication>
Server tashqaridan ochiq listen(port, "0.0.0.0") proxy orqasida 127.0.0.1 (konteynerdan tashqarida)
Logning birinchi qatorlari boshqa formatda bufferLogs yo'q bufferLogs: true + useLogger

7. Mashqlar

Mashqlar kurs/mashqlar/30/06-bootstrap/ da.

1-mashq (oson): qaysi sozlama?

  • Webhook imzosi uchun xom tana — NestFactory.create(AppModule, { : true })
  • Hamma yo'l oldiga /api — app.("api")
  • Konteyner ichida hamma interfeysda tinglash — app.listen(3000, "")
  • Nest'ning o'z JSON parser'ini o'chirish — { : false }
  • /v1/vazifalar va /vazifalar — bitta controller: defaultVersion: ["1", ]
Yechim
  • rawBody — req.rawBody da Buffer.
  • setGlobalPrefix — exclude bilan istisno qo'shiladi.
  • 0.0.0.0 — faqat konteyner yoki ishonchli tarmoqda; oddiy serverda proxy orqasida 127.0.0.1.
  • bodyParser — vazifalar-api da o'z parser'imiz uchun.
  • VERSION_NEUTRAL — "versiyasiz" ham shu controller'ga.

2-mashq (o'rta): prefiks modeli

prefiksQosh(marshrutlar, prefiks, istisno) funksiyasini yozing: har marshrut yo'li oldiga /${prefiks} qo'shsin, istisno ro'yxatidagilardan tashqari. Keyin top(jadval, url) — mos yo'l bo'lsa "200 <yo'l>", bo'lmasa: url prefiks bilan boshlansa "404 JSON (Nest)", aks holda "404 HTML (Express)". Sinov: /vazifalar, /salomatlik (istisno), so'rovlar /vazifalar, /api/vazifalar, /salomatlik, /api/yoq. Ishora: startsWith (satr metodlari), includes.

Yechim
js
function prefiksQosh(marshrutlar, prefiks, istisno) {
  return marshrutlar.map((y) =>
    istisno.includes(y) ? y : `/${prefiks}${y}`);
}

function top(jadval, url, prefiks) {
  if (jadval.includes(url)) return `200 ${url}`;
  return url.startsWith(`/${prefiks}/`)
    ? "404 JSON (Nest)"
    : "404 HTML (Express)";
}

const jadval = prefiksQosh(
  ["/vazifalar", "/salomatlik"], "api", ["/salomatlik"],
);
console.log(jadval.join(", "));
for (const url of
  ["/vazifalar", "/api/vazifalar", "/salomatlik", "/api/yoq"]) {
  console.log(url, "→", top(jadval, url, "api"));
}

Konsolda:

text
/api/vazifalar, /salomatlik
/vazifalar → 404 HTML (Express)
/api/vazifalar → 200 /api/vazifalar
/salomatlik → 200 /salomatlik
/api/yoq → 404 JSON (Nest)

Model darsdagi tajribani takrorlaydi: prefiksdan tashqaridagi yo'l Nest'ning JSON 404 iga yetmaydi. /api/yoq esa prefiks ostida — u Nest'ning ishlovchisiga tushadi. Haqiqiy Nest'da 404 javobining shakli global filtrga bog'liq; vazifalar-api da u Problem Details.

3-mashq (qiyin): URI versiyalash modeli

versiyaTop(url, controllerlar) funksiyasini yozing. Har controller: { yol: "vazifalar", versiyalar: ["1", "neytral"] }. URL /v<N>/<yol>… shaklida bo'lsa — versiya N, aks holda — "neytral". Mos controller topilsa "<yol> v<N>" yoki "<yol> versiyasiz (eskirgan)" qaytarsin, topilmasa "404". Sinov: /v1/vazifalar/5, /vazifalar/5, /v2/vazifalar, /v2/teglar (teglar — ["1", "2"]). Ishora: /^\/v(\d+)(\/.*)$/ (RegExp), split("/").

Yechim
js
function versiyaTop(url, controllerlar) {
  const m = /^\/v(\d+)(\/.*)$/.exec(url);
  const versiya = m ? m[1] : "neytral";
  const qolgan = m ? m[2] : url;
  const yol = qolgan.split("/")[1];
  const c = controllerlar.find(
    (k) => k.yol === yol && k.versiyalar.includes(versiya),
  );
  if (!c) return "404";
  return versiya === "neytral"
    ? `${yol} versiyasiz (eskirgan)`
    : `${yol} v${versiya}`;
}

const controllerlar = [
  { yol: "vazifalar", versiyalar: ["1", "neytral"] },
  { yol: "teglar", versiyalar: ["1", "2"] },
];
const urllar = [
  "/v1/vazifalar/5", "/vazifalar/5", "/v2/vazifalar", "/v2/teglar",
];
for (const url of urllar) {
  console.log(url, "→", versiyaTop(url, controllerlar));
}

Konsolda:

text
/v1/vazifalar/5 → vazifalar v1
/vazifalar/5 → vazifalar versiyasiz (eskirgan)
/v2/vazifalar → 404
/v2/teglar → teglar v2

"neytral" — Nest'dagi VERSION_NEUTRAL ning o'rni. /v2/vazifalar — 404: bunday versiya yo'q. Haqiqiy Nest bundan ko'proq ishlaydi (sarlavha va media turi bo'yicha versiya, bitta metodga alohida versiya), lekin URI rejimining mantig'i shu.

8. Loyiha qadami: Nest bootstrap va Express'ni o'chirish

8.1 Reja

Bu dars — ko'chishning oxirgi qismi. Uch ish:

  1. Ilovani yig'ish Nest'ga o'tadi: app.module.ts, ilova.ts, main.ts, ishga-tushir.ts, yangi problem.filter.ts va tana-xatosi.ts.
  2. Eski Express fayllari o'chadi.
  3. 25–27 testlari Vitest'ga o'tadi, tanalari o'zgarmaydi.

Kanon fayllarida qatorlar uzun edi — telefon uchun bo'ldik (izohlar qisqartirildi, kod ma'nosi aynan). Bo'lingan nusxani biz kanon nusxasida tsc --noEmit, ESLint, Vitest va build bilan tekshirdik.

8.2 app.module.ts

ts
// app.module.ts — ildiz modul: ilova shu moduldan boshlab yig'iladi
// (imports daraxti). forRoot(...) — tashqarida (omborYarat)
// yaratilgan bog'liqliklarni global modul orqali beradi
import { Module } from "@nestjs/common";
import type { DynamicModule } from "@nestjs/common";
import { BogliqliklarModule } from "./bogliqliklar.module.ts";
import type { TashqiBogliqliklar } from "./bogliqliklar.module.ts";
import { VazifalarModule } from "./vazifalar/vazifalar.module.ts";
import { EksportModule } from "./eksport/eksport.module.ts";

@Module({})
export class AppModule {
  static forRoot(b: TashqiBogliqliklar): DynamicModule {
    return {
      module: AppModule,
      imports: [
        BogliqliklarModule.forRoot(b),
        VazifalarModule,
        EksportModule,
      ],
    };
  }
}

SalomController ketdi — skeletning vazifasi tugadi. forRoot(b) — 3-darsdagi "dinamik modul — bir qarash" ko'rinishi. U o'tgan darsdagi xatoni yopadi: endi BogliqliklarModule daraxtda va VAZIFA_OMBORI ko'rinadi.

8.3 ilova.ts

ts
// ilova.ts — Nest ilovasi (25-qism ilova.ts ning o'rni): Express
// middleware'lari → Nest marshrutlari → Problem Details filtri.
// listen() YO'Q — server ishga-tushir.ts da, testlar ilovani portsiz
// ham ishlata oladi (supertest: app.getHttpServer())
import { NestFactory } from "@nestjs/core";
import { VERSION_NEUTRAL, VersioningType } from "@nestjs/common";
import type { LogLevel } from "@nestjs/common";
import type {
  NestExpressApplication,
} from "@nestjs/platform-express";
import express from "express";
import cors from "cors";
import helmet from "helmet";
import { join } from "node:path";
import { AppModule } from "./app.module.ts";
import { sorovId } from "./umumiy/sorov-id.ts";
import {
  idempotentlik, XotiraIdempotentlikOmbori,
} from "./umumiy/idempotentlik.ts";
import type { IdempotentlikOmbori } from "./umumiy/idempotentlik.ts";
import { JSON_TURLARI } from "./umumiy/json-kerak.ts";
import { eskirgan, ESKI_YOLLAR } from "./umumiy/eskirgan.ts";
import { tanaXatosi } from "./umumiy/tana-xatosi.ts";
import { ProblemDetailsFilter } from "./umumiy/problem.filter.ts";
import { joriyLog, logYarat, sorovLogi } from "./umumiy/log.ts";
import type { Log } from "./umumiy/log.ts";
import type { Xabarchi } from "./umumiy/webhook.ts";
import type { VazifaOmbori } from "./vazifalar/ombor.ts";

export type Bogliqliklar = {
  ombor: VazifaOmbori;
  /** CORS ruxsat etilgan origin'lar (frontend: Vite dev, Pages) —
   * bo'sh massiv: CORS sarlavhasi yo'q */
  corsOriginlar?: readonly string[];
  /** Idempotency-Key javoblari */
  idempotentlikOmbori?: IdempotentlikOmbori;
  /** Pino logger (sukut — "silent": testlarda jim) */
  log?: Log;
  /** tashqi xabar (WEBHOOK_URL bo'lsa) */
  xabarchi?: Xabarchi;
  /** foydalanuvchi sanalari mintaqasi (?dan=, ?gacha=) */
  tz?: string;
  /** Express "trust proxy": false — proxy yo'q */
  trustProxy?: boolean | number | string;
  /** HSTS faqat HTTPS orqasida (proxy) yoqiladi */
  hsts?: boolean;
  /** Nest'ning o'z logger'i (bootstrap xabarlari) — sukut o'chiq;
   * server ["error", "warn"] beradi */
  nestLogi?: LogLevel[] | false;
};

export async function ilovaYarat({
  ombor,
  corsOriginlar = [],
  idempotentlikOmbori = new XotiraIdempotentlikOmbori(),
  log = logYarat("silent"),
  xabarchi,
  tz = "Asia/Tashkent",
  trustProxy = false,
  hsts = false,
  nestLogi = false,
}: Bogliqliklar): Promise<NestExpressApplication> {
  // bodyParser: false — Nest o'z JSON parser'ini qo'ymasin: biznikida
  // limit (16 KB) va merge-patch+json turi bor
  const app = await NestFactory.create<NestExpressApplication>(
    AppModule.forRoot({ ombor, log, xabarchi, sozlamalar: { tz } }),
    { bodyParser: false, logger: nestLogi },
  );
  app.set("trust proxy", trustProxy);
  app.disable("x-powered-by"); // "Express" ekanini oshkor qilmaymiz
  // app.use() — Express middleware'lari, Nest marshrutlaridan OLDIN,
  // shu tartibda (25-qism ilova.ts bilan bir xil)
  app.use(sorovId());
  // so'rov logi + AsyncLocalStorage konteksti (sorovId) — qolgan
  // hamma middleware, guard, handler shu kontekst ichida
  app.use(sorovLogi(log));
  app.use(helmet({ strictTransportSecurity: hsts }));
  app.use(
    cors({
      origin: [...corsOriginlar],
      allowedHeaders: [
        "Content-Type", "Idempotency-Key", "X-Request-Id", "If-Match",
        "If-None-Match",
      ],
      exposedHeaders: [
        "Location", "X-Request-Id", "Idempotency-Replayed", "ETag",
        "Link", "Deprecation", "Sunset",
      ],
      maxAge: 600,
    }),
  );
  app.useStaticAssets(join(import.meta.dirname, "..", "public"));
  app.use(express.json({ limit: "16kb", type: JSON_TURLARI }));
  app.use(tanaXatosi);
  // Eski versiyasiz yo'llar — ishlaydi, lekin eskirgan: Deprecation +
  // Sunset + vorisga havola
  app.use(eskirgan(ESKI_YOLLAR));
  // Idempotency-Key — faqat vazifalar to'plamida (25-qismdagidek);
  // keyinroq Nest middleware'iga (MiddlewareConsumer) o'tadi
  app.use(
    ["/v1/vazifalar", "/vazifalar"],
    idempotentlik(idempotentlikOmbori),
  );
  // Versiya URI'da: /v1/... ; VERSION_NEUTRAL — versiyasiz eski
  // yo'l ham shu controller'ga (Express'da v1 router ikki joyga
  // ulangan edi)
  app.enableVersioning({
    type: VersioningType.URI,
    defaultVersion: ["1", VERSION_NEUTRAL],
  });
  app.useGlobalFilters(new ProblemDetailsFilter((_id, xato) => {
    joriyLog(log).error({ err: xato }, "kutilmagan xato");
  }));
  await app.init();
  return app;
}

25-qismdagi bilan farqlar:

25–27 (Express) Nest
function ilovaYarat(...): Express async function …: Promise<NestExpressApplication>
express() NestFactory.create(AppModule.forRoot(…), …)
vazifaXizmati(ombor, …) DI (BogliqliklarModule)
v1.use(...), app.use("/v1", v1) controller'lar + enableVersioning
app.use(topilmadi) Nest router'ining 404 i → filtr
app.use(xatoIshlovchi(...)) useGlobalFilters(new ProblemDetailsFilter(...))

app.init() — marshrutlar va middleware'lar shu yerda ulanadi. listen hali yo'q: testlar ilovani portsiz ishlatadi (app.getHttpServer()). ilovaYarat endi async — Nest yaratish Promise qaytaradi. Bu kichik farq testlarga ta'sir qiladi — «Eski fayllar va testlar» bo'limida.

8.4 problem.filter.ts va tana-xatosi.ts

ts
// problem.filter.ts — global exception filter: HAMMA xato bir joyda
// Problem Details'ga aylanadi (25-qism xato-ishlovchi.ts ning Nest
// shakli). @Catch() — argumentsiz: har qanday xato (IlovaXatosi, Nest
// HttpException, kutilmagan TypeError…)
import {
  Catch, HttpException, NotFoundException,
} from "@nestjs/common";
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
import type { Request, Response } from "express";
import { IlovaXatosi } from "./xatolar.ts";
import { problemYasa, problemYubor } from "./problem.ts";

export type XatoLogi = (sorovId: string, xato: unknown) => void;

@Catch()
export class ProblemDetailsFilter implements ExceptionFilter {
  constructor(
    private readonly logla: XatoLogi = (id, x) => {
      console.error(`[${id}]`, x);
    },
  ) {}

  catch(xato: unknown, host: ArgumentsHost): void {
    const http = host.switchToHttp();
    const req = http.getRequest<Request>();
    const res = http.getResponse<Response>();
    if (res.headersSent) {
      // javob yarmida — status o'zgartirib bo'lmaydi: log va
      // ulanishni yopamiz
      this.logla(req.sorovId, xato);
      res.end();
      return;
    }
    const umumiy = {
      instance: req.originalUrl, sorovId: req.sorovId,
    };
    if (xato instanceof IlovaXatosi) {
      res.set(xato.httpSarlavhalar);
      problemYubor(res, problemYasa(xato.holat, {
        ...umumiy, kod: xato.kod, sarlavha: xato.sarlavha,
        detail: xato.message, xatolar: xato.xatolar,
      }));
      return;
    }
    // Nest router'i mos marshrut topmasa NotFoundException
    // ("Cannot GET /yoq") tashlaydi — o'zimizning xabar bilan
    // (Express'dagidek)
    if (xato instanceof NotFoundException) {
      problemYubor(res, problemYasa(404, {
        ...umumiy,
        detail: `${req.method} ${req.path} — bunday manzil yo'q`,
      }));
      return;
    }
    // Boshqa Nest HttpException'lari (kutubxonalar tashlaydi) —
    // status saqlanadi, 5xx tafsiloti yashiriladi
    if (xato instanceof HttpException && xato.getStatus() < 500) {
      problemYubor(res, problemYasa(xato.getStatus(), {
        ...umumiy, detail: xato.message,
      }));
      return;
    }
    // Kutilmagan (dasturchi xatosi, DB uzilishi…): tafsilot (stack,
    // xabar) mijozga BERILMAYDI — faqat logga
    this.logla(req.sorovId, xato);
    problemYubor(res, problemYasa(500, {
      ...umumiy,
      detail: "Kutilmagan xato. sorovId bilan murojaat qiling.",
    }));
  }
}

Mantiq 25-qismdagi xatoIshlovchi bilan bir xil, ikki qo'shimcha bilan: Nest'ning NotFoundException i va boshqa HttpException lar. host.switchToHttp() — filtr HTTP'dan boshqa kontekstda ham ishlay oladi (mikroservis, WebSocket), shuning uchun so'rov va javob shu orqali olinadi. Bu tafsilotlar — Exception filter'lar darsida.

ts
// tana-xatosi.ts — Express xato middleware'i (4 argument):
// body-parser (express.json) va serve-static xatolarini IlovaXatosi
// ga aylantiradi. Nima uchun kerak: Nest Express xatolarini o'z
// filtriga uzatishdan oldin "xaritalaydi" — SyntaxError (yaroqsiz
// JSON) → BadRequestException(err.message), va body-parser'ning
// `type` maydoni ("entity.parse.failed") yo'qoladi. Shuning uchun
// xatoni Nest'dan OLDIN o'zimiz tarjima qilamiz
import type { ErrorRequestHandler } from "express";
import { IlovaXatosi } from "./xatolar.ts";

// body-parser xatolari (`type` maydoni) → o'zbekcha tafsilot
const TANA_XATOLARI: Record<string, string> = {
  "entity.parse.failed": "Tana yaroqli JSON emas",
  "entity.too.large": "Tana 16 KB dan katta",
  "encoding.unsupported": "Kodlash qo'llab-quvvatlanmaydi",
  "charset.unsupported": "Belgilar to'plami qo'llab-quvvatlanmaydi",
};

/** http-errors (body-parser, serve-static): status + expose */
function httpErrorsmi(
  x: unknown,
): x is Error & { status: number; expose: boolean; type?: unknown } {
  return x instanceof Error &&
    "status" in x && typeof x.status === "number" &&
    "expose" in x && typeof x.expose === "boolean";
}

export const tanaXatosi: ErrorRequestHandler = (
  xato: unknown, _req, _res, next,
) => {
  if (httpErrorsmi(xato) && xato.status < 500 && xato.expose) {
    const tur = typeof xato.type === "string" ? xato.type : "";
    next(new IlovaXatosi(
      xato.status, TANA_XATOLARI[tur] ?? xato.message,
      { cause: xato },
    ));
    return;
  }
  next(xato);
};

TANA_XATOLARI va httpErrorsmi — 25-qismdagi xato-ishlovchi.ts dan aynan ko'chdi. { cause: xato } — asl xato yo'qolmasin, logda ko'rinsin (error.cause).

8.5 main.ts va ishga-tushir.ts

main.ts — 25–27 dagi server.ts ning o'rni. Mazmuni deyarli o'sha:

ts
// main.ts — kirish nuqtasi (25–27 server.ts ning o'rni): config
// (fail-fast) → ombor → Nest ilovasi → server → jarayon hodisalari.
// Ishga tushirish: npm run build && npm start
// (node --env-file-if-exists=.env dist/main.js)
import "reflect-metadata";
import { configYukla, ConfigXatosi } from "./config.ts";
import { ishgaTushir, omborYarat } from "./ishga-tushir.ts";
import { jarayonniBoshqar } from "./jarayon.ts";
import type { Config } from "./config.ts";
import { joriyLog, logYarat } from "./umumiy/log.ts";
import { webhookXabarchi } from "./umumiy/webhook.ts";

let config: Config;
try {
  config = configYukla(process.env);
} catch (x) {
  if (!(x instanceof ConfigXatosi)) throw x;
  console.error(x.message);
  // EX_CONFIG (sysexits.h) — supervisor qayta-qayta ishga
  // tushirmasin, sozlamani tuzatish kerak
  process.exit(78);
}

const log = logYarat(config.LOG_LEVEL);
const saqlash = await omborYarat(config, log);
const { server, manzil, app } = await ishgaTushir({
  port: config.PORT,
  host: config.HOST,
  bogliqliklar: {
    ombor: saqlash.ombor,
    corsOriginlar: config.CORS_ORIGINS,
    tz: config.APP_TZ,
    trustProxy: config.TRUST_PROXY,
    hsts: config.HSTS,
    log,
    nestLogi: ["error", "warn"],
    ...(config.WEBHOOK_URL === undefined ? {} : {
      xabarchi: webhookXabarchi(
        config.WEBHOOK_URL, { log: () => joriyLog(log) },
      ),
    }),
  },
});
log.info(
  { manzil, muhit: config.NODE_ENV, saqlash: config.SAQLASH },
  `vazifalar-api: ${manzil}`,
);
jarayonniBoshqar(server, {
  info: (x) => { log.info(x); },
  // fatal sinxron yoziladi (pino stdout'ga sinxron) — exit'dan oldin
  // yo'qolmaydi
  fatal: (x, xato) => { log.fatal({ err: xato }, x); },
}, {
  // server allaqachon yopilgan (yop) — app.close() Nest modullarining
  // yopish hook'larini chaqiradi, keyin pool/redis
  oxirida: async () => {
    await app.close();
    await saqlash.yop();
  },
});

Uch farq: birinchi qator import "reflect-metadata" (Nest metama'lumoti uchun), nestLogi va yopishda app.close(). ishgaTushir endi app ni ham qaytaradi.

ishga-tushir.ts da omborYarat o'zgarmaydi. O'zgaradigani — ishgaTushir funksiyasi:

ts
type Sozlama = {
  port?: number;
  host?: string;
  bogliqliklar?: Bogliqliklar;
};

export async function ishgaTushir({
  port = 0,
  host = "127.0.0.1",
  bogliqliklar = { ombor: new XotiraOmbori() },
}: Sozlama = {}): Promise<Ishlayotgan> {
  const app = await ilovaYarat(bogliqliklar);
  // Nest http serverni NestFactory.create() da o'zi yaratadi —
  // timeout'larni listen() dan OLDIN shu obyektga beramiz
  const server = app.getHttpServer();
  // Load balancer (nginx, ALB) bo'sh ulanishni ~60 s ushlaydi — Node
  // undan KEYIN yopsin
  server.keepAliveTimeout = 65_000;
  // Slowloris: sarlavhalar 10 s ichida to'liq kelmasa — 408
  server.headersTimeout = 10_000;
  server.requestTimeout = 30_000;
  await app.listen(port, host); // EADDRINUSE — rad etilgan Promise
  const { port: haqiqiy } = server.address() as AddressInfo;
  return { server, manzil: `http://${host}:${String(haqiqiy)}`, app };
}

25-qismda createServer({ keepAliveTimeout, … }, ilova) o'zimiz yaratardik. Nest serverni create da o'zi yaratadi — timeout'lar o'sha obyektga listen dan oldin yoziladi (Server timeout'lari va keep-alive). Ishlayotgan turiga app: NestExpressApplication maydoni qo'shiladi; faylning boshidagi createServer importi ketadi, NestExpressApplication turi keladi.

8.6 Eski fayllar va testlar

Endi npm run tip ni yurgizsak (biz kanon nusxasida yurgizdik):

text
src/vazifalar/marshrut.ts(6,10): error TS2305: Module '"../umumiy/json-kerak.ts"' has no exported member 'jsonKerak'.
test/mock.test.ts(20,17): error TS2345: Argument of type 'Promise<NestExpressApplication<Server<typeof IncomingMessage, typeof ServerResponse>>>' is not assignable to parameter of type 'App'.
test/supertest.test.ts(14,31): error TS2345: Argument of type 'Promise<NestExpressApplication<Server<typeof IncomingMessage, typeof ServerResponse>>>' is not assignable to parameter of type 'App'.

(Jami 18 ta: bitta TS2305 va 17 ta TS2345 — mock.test.ts da 9, supertest.test.ts da 8; qolganlari bir xil.)

Tarjimasi: TS2305 — "json-kerak.ts modulida jsonKerak nomli eksport yo'q". json-kerak.ts da endi faqat jsonTanaKerak qoldi (eski middleware olib tashlandi). Uni so'rayotgan yagona fayl — eski Express routeri marshrut.ts. TS2345 — "Promise<NestExpressApplication> turidagi argument App turiga mos emas". Testlar request(ilovaYarat(...)) qilardi, endi ilovaYarat Promise qaytaradi.

Birinchi xato — "eski fayllarni o'chiring" signali:

bash
git rm src/server.ts src/vazifalar/marshrut.ts \
  src/vazifalar/xizmat.ts src/eksport/eksport.ts \
  src/umumiy/xato-ishlovchi.ts \
  src/salom.controller.ts src/salom.controller.spec.ts

Ikkinchisi uchun test/yordamchi.ts ga kichik yordamchi:

ts
/** supertest uchun: Nest ilovasi (init qilingan, listen qilinmagan)
 * http serveri — request(server) o'zi vaqtinchalik tinglatadi */
export async function httpIlova(b: Bogliqliklar): Promise<Server> {
  return (await ilovaYarat(b)).getHttpServer();
}

Testlarda ilovaYarat({...}) → await httpIlova({...}). Qolgan o'zgarishlar mexanik — node:test dan Vitest'ga:

node:test (25–27) Vitest
import { test } from "node:test" import { test } from "vitest"
before, after beforeAll, afterAll
test(nom, { skip: "…" }, fn) test.skip(nom, fn)
t.mock.fn(...) vi.fn(...)
t.mock.method(obyekt, "m") vi.spyOn(obyekt, "m")
mock.callCount() mock.calls.length
t.mock.timers.enable({ apis: ["Date"] }) vi.useFakeTimers({ toFake: ["Date"] })

node:assert/strict qoladi — Vitest ichida ham ishlaydi. Test tanalari (nimani tekshirishi) o'zgarmaydi. Kanon bu almashtirishni kichik skript bilan qildi; siz qo'lda yoki muharriringizning "qidir va almashtir"i bilan qilasiz.

Uch joyda qo'shimcha ish bor:

  1. jarayon.test bola jarayonda node qolip/halokat.ts ni yurgizardi. Endi u Nest kodini import qiladi — type stripping @ da yiqiladi (Nest CLI darsidagi xato). Yangi test/qolip/yurgiz.mjs faylni Vite orqali ishga tushiradi (Vitest bilan bir xil o'girish):

    js
    import { runnerImport } from "vite";
    
    await runnerImport(process.argv[2], { logLevel: "silent" });

    Vitest'ning sukut test vaqti — 5 s (node:test da chegara yo'q edi). Bola jarayon ~2 s da ko'tariladi, shuning uchun testga { timeout: 20_000 } beriladi.

  2. mock.test — t.mock testdan keyin o'zi tiklanardi. Vitest'da afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers(); }) kerak.

  3. migratsiya.test — 2-darsda va'da qilingan beqaror test. Kanon sababini topdi: afterAll da pool.end() mijoz soketi yopilishini kutmaydi, darhol keyingi DROP DATABASE … WITH (FORCE) o'sha seansni o'ldiradi. Mijoz FATAL 57P01 oladi, uni tinglovchi yo'q — ushlanmagan xato. Vitest buni Unhandled Errors deb ko'rsatadi va exit 1. Bu xato 27-qismda ham bor edi, node:test uni kamdan-kam ko'rsatardi. Tuzatish — FORCE siz o'chirish, seans hali bo'lsa (55006) qisqa kutib qayta urinish:

    ts
    for (let i = 0; ; i++) {
      try {
        await admin.query(
          "DROP DATABASE IF EXISTS vazifalar_test_mig",
        );
        break;
      } catch (x) {
        const kod = (x as { code?: string }).code;
        if (i >= 40 || kod !== "55006") throw x;
        await new Promise((r) => setTimeout(r, 50));
      }
    }

    Kanon tuzatishdan keyin 6 marta ketma-ket yurgizdi — 111/111.

Konfiguratsiya: vitest.config.ts da include: ["test/**/*.test.ts", "src/**/*.spec.ts"] (25–27 fayl nomlari saqlandi); package.json dan start:express va test:eski skriptlari olib tashlanadi, check — npm run tip && npm run lint && npm test. eslint.config.js dagi no-floating-promises izohi yangilanadi: Vitest'ning test() i void qaytaradi, qoida esa qoladi.

8.7 Tekshiruv

bash
npm run check
npm run build
SAQLASH=xotira npm start

Kanon tekshiruvi (Node 24.21.0, NestJS 12.2.0, Vitest 5.0.5, PostgreSQL 18.6):

text
--- npm run tip
exit 0 (4485 ms)
--- npm run lint
exit 0 (9198 ms)
--- npm run test
 ↓ test/konteyner.test.ts (1 test | 1 skipped)
 Test Files  20 passed | 1 skipped (21)
      Tests  111 passed | 1 skipped (112)
exit 0 (7300 ms)
--- npm run build
exit 0 (5391 ms)

25–27 ning 112 testi Nest ilovasida o'tdi: shartnoma, supertest, xatolar, idempotentlik, CORS, ETag/304, If-Match, eskirgan yo'l, eksport, log korrelyatsiyasi. Bitta SKIP — Testcontainers (TESTCONTAINERS=1 bilan alohida, 10/10). Vitest 7.3 s; 27-qismdagi node:test — 18.7 s.

Biz ham kanon nusxasida, bazasiz yurgizdik: tip 0, lint 0, Vitest 71 o'tdi + 5 fayl SKIP (Postgres va Redis kerak). Birinchi yugurishda jarayon.test 20 s ga yetib yiqildi (sovuq Vite yuklanishi, mashina band edi), keyingi ikki yugurishda o'tdi. Sizda ham shunday bo'lsa — qayta yurgizing; doim yiqilsa, xabarini o'qing.

Jonli so'rovlar (biz ishga tushirdik, SAQLASH=xotira; helmet sarlavhalari, Date, ETag, X-Request-Id olib tashlandi):

text
$ curl -X POST -H 'Content-Type: application/json' -d '{"matn":"Non olish #bozor"}' /v1/vazifalar
HTTP/1.1 201 Created
Location: /v1/vazifalar/1
{"id":1,"matn":"Non olish #bozor","bajarildi":false,"teglar":["bozor"],…,"versiya":1}

$ curl /vazifalar/1
HTTP/1.1 200 OK
Deprecation: @1792800000
Sunset: Sun, 24 Jan 2027 00:00:00 GMT
Link: </v1/vazifalar/1>; rel="successor-version"

$ curl -X POST -H 'Content-Type: application/json' -d '{"format":"csv"}' /v1/eksportlar
HTTP/1.1 202 Accepted
Location: /v1/eksportlar/ca52023c-1aca-4836-8f63-94929713096c
Retry-After: 1

$ curl /v1/teglar
HTTP/1.1 200 OK
{"teglar":[{"teg":"bozor","soni":1}]}

Eksport ID — randomUUID(), sizda boshqa chiqadi. Kanon xuddi shu ko'chishni 9 so'rov bilan uch saqlash rejimida (Postgres, Prisma, Prisma + kesh) tekshirgan: Express versiyasidan farq faqat port va vaqt qatorlarida.

8.8 Commit

bash
git add -A
M="feat!: vazifalar-api Nest'ga ko'chdi — modullar,"
M+=" controller'lar, xizmat; testlar Vitest'da,"
M+=" HTTP xulqi o'zgarmadi"
git commit -m "$M"

feat! — undov belgisi "buzuvchi o'zgarish": tashqi API o'zgarmadi, lekin ishga tushirish usuli o'zgardi (endi build kerak, start:express yo'q). Prod bog'liqliklar soni: 27-qismda 10, endi 15.

9. Real ishda

  • main.ts ning hajmi — loyihaning sog'lig'i belgisi. Odatda 20–60 qator: create, global sozlamalar, listen. Unda biznes mantiq bo'lmaydi.
  • setGlobalPrefix("api") — frontend va API bitta domenda bo'lganda keng tarqalgan. Salomatlik yo'li uchun exclude deyarli doim yoziladi.
  • rawBody: true — Stripe, Payme kabi webhook qabul qiladigan loyihalarda majburiy sozlama.
  • Ko'chishda (Express → Nest) eng ko'p vaqt middleware tartibi va xato formatiga ketadi. Shartnoma testlari bo'lmasa, farqlarni mijozlar topadi.
  • Intervyu: "NestFactory.create nima qiladi?", "Global prefiks va versiyalash farqi?", "Nest'da raw body qanday olinadi?", "Nima uchun listen da host muhim?".

Xulosa

  • NestFactory.create modul daraxtini yig'adi, nusxalarni yaratadi va marshrutlarni ulaydi; listen portni ochadi.
  • Yaratish sozlamalari (bodyParser, rawBody, bufferLogs, logger) — create da; global sozlamalar (setGlobalPrefix, enableCors, enableVersioning, useGlobalFilters) — create va listen orasida.
  • Global prefiks Nest'ning 404 ishlovchisini ham prefiks ostiga qo'yadi — tashqaridagi yo'llar Express HTML'ini oladi.
  • Nest Express xatolarini filtrdan oldin xaritalaydi: type yo'qoladi, katta tana 500 bo'lib qoladi — tarjima middleware'i bilan Nest'gacha ushlang.
  • vazifalar-api to'liq Nest'da: Express routerlari o'chdi, 112 test Vitest'da o'tdi, HTTP javoblari o'zgarmadi.

Keyingi dars: Request lifecycle — so'rov middleware, guard, interceptor, pipe va filtrdan qaysi tartibda o'tishini kanon tajribasining haqiqiy izi bilan ko'ramiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
NestJS bootstrap: main.ts, NestFactory.create va ilova sozlamalari — IlmHamroh