IlmHamroh
JavaScript Full-stack/30-qism. NestJS21/65-dars22 daqiqa
Mundarija (29)

Middleware Nest'da: NestMiddleware, MiddlewareConsumer, forRoutes va ikki marta ishlash tuzog'i

Qisqacha: Nest'da middleware — o'sha Express middleware'i ((req, res, next)), lekin ikki shaklda: funksional (oddiy funksiya) va klass (NestMiddleware ning use() metodi, DI bilan bog'liqlik oladi). Ular modulda ulanadi: modul NestModule ni bajaradi va configure(consumer) ichida consumer.apply(X).forRoutes(...) yozadi; exclude(...) — istisno yo'llar. Har marshrutga kerak bo'lgan, 404 ga ham ishlashi kerak bo'lganlar — app.use() da qoladi. Kanon ikki haqiqiy tuzoq topgan: forRoutes(Controller) bilan middleware bitta so'rovda ikki marta ishlashi mumkin, yo'l bilan ulanganda esa req.path kesiladi.

Bu darsda

  • Funksional va klass middleware'ni yozasiz va klass middleware'ga DI bilan bog'liqlik berasiz.
  • NestModule.configure, MiddlewareConsumer, forRoutes va exclude bilan middleware'ni kerakli marshrutlarga ulaysiz.
  • app.use() va modul middleware'i farqini, qaysi birini qachon tanlashni bilasiz.
  • Nest 12.2 ning ikki kamchiligini haqiqiy test xatolarida ko'rasiz va tuzatasiz.
  • vazifalar-api ning idempotentlik va eskirgan yo'l middleware'larini Nest modullariga ko'chirasiz.

Oldin bilishingiz kerak: Middleware va request lifecycle, Idempotency-Key, API versiyalash va deprecation, Request lifecycle, Custom provider'lar.

Validatsiya darslari tugadi — kirish tekshiruvi pipe'da, 415 — guard'da. Endi so'rov hayotining eng boshiga qaytamiz: middleware.

1. Nega bu kerak?

25-qismda vazifalar-api ning hamma "o'rta qatlami" middleware edi: sorovId, helmet, cors, express.json, idempotentlik, eskirgan yo'llar. Nest'ga ko'chganimizda ularni Bootstrap darsida app.use() ga o'zgarishsiz qo'ydik — ishlaydi.

Lekin ikkitasi noqulay qoldi:

  • Idempotentlik omborga muhtoj (IdempotentlikOmbori — testda soxta soatli, 27-qismda Redis). app.use(idempotentlik(ombor)) — ombor ilova.ts da qo'lda yaratiladi va uzatiladi. Nest'da esa bog'liqliklar DI konteyneridan keladi.
  • Qaysi yo'llarga — app.use(["/v1/vazifalar", "/vazifalar"], ...): yo'llar qo'lda, satr bilan. Controller'ning yo'li o'zgarsa — bu satr eskiradi.

Nest'da middleware'ni modul ulaydi va uni controller'ga bog'lash mumkin. DI ham ishlaydi. Bu dars — shu usul va uning ikki kutilmagan tomoni.

O'xshatish: bino kirish eshigidagi qorovul va qavatdagi kotiba. Qorovul (app.use) hamma kiruvchini ko'radi — hatto adashib kirganlarni ham. Kotiba (modul middleware) faqat o'z bo'limiga kelganlarni qabul qiladi va bo'lim jurnalidan (DI) foydalanadi.

Express (25-qism) Nest modul middleware'i
Ulash app.use(yollar, mw) yoki router.use(mw) consumer.apply(mw).forRoutes(...)
Bog'liqlik yopilish orqali qo'lda (idempotentlik(ombor)) constructor orqali DI
Yo'l satr controller klassi, satr yoki { path, method }
Kutilmagan farq — bitta so'rovda ikki marta ishlashi mumkin (pastda)

2. Ikki shakl

2.1 Funksional middleware

Bu — 25-qismdan tanish Express middleware'i (Middleware va request lifecycle). Nest'da hech narsa o'zgarmaydi:

ts
function funksional(req: Request, res: Response, next: NextFunction) {
  res.set('X-Funksional', 'ha');
  next();
}

Bog'liqlik kerak bo'lsa — yopilish (Closure) orqali: eskirgan(ESKI_YOLLAR) — funksiya middleware qaytaradi. Kanonning eskirgan i aynan shunday.

2.2 Klass middleware

ts
@Injectable()
class KlassMiddleware implements NestMiddleware {
  constructor(private readonly soat: Soat) {}

  use(req: Request, _res: Response, next: NextFunction) {
    console.log(`klass ${req.method} ${req.originalUrl} ` +
      `(${this.soat.hozir()})`);
    next();
  }
}
  • NestMiddleware — bitta use(req, res, next) metodi bor interfeys.
  • @Injectable() — Nest uni provider kabi yaratadi va constructor'iga bog'liqliklarni beradi. Soat — modulning providers idagi xizmat.

Klass middleware'ning yagona afzalligi — DI. Bog'liqlik kerak bo'lmasa, funksional yozing — u soddaroq. Nest hujjati ham shuni tavsiya qiladi.

Klass middleware'ning yana bir xususiyati: u provider kabi bir marta yaratiladi va hamma so'rovlarda bitta nusxa ishlaydi. Shuning uchun klass maydonida so'rovga oid narsa (masalan, this.joriyKalit) saqlamang — parallel so'rovlar bir-birining qiymatini bosib ketadi. So'rovga oid ma'lumot req da yoki mahalliy o'zgaruvchida turadi.

3. Ulash: configure va MiddlewareConsumer

3.1 NestModule

Middleware @Module({...}) ichida emas, modul klassining configure() metodida ulanadi:

ts
@Module({ controllers: [VazifalarController], providers: [Soat] })
class M implements NestModule {
  configure(c: MiddlewareConsumer) {
    c.apply(KlassMiddleware)
      .exclude({
        path: 'vazifalar/sogliq', method: RequestMethod.GET,
      })
      .forRoutes(VazifalarController);
    c.apply(yolKor).forRoutes('*splat');
    c.apply(funksional).forRoutes({
      path: 'vazifalar',
      method: RequestMethod.POST,
    });
  }
}
  • apply(...) — bir yoki bir nechta middleware (vergul bilan, tartibda bajariladi).
  • forRoutes(...) — qayerga: controller klassi, yo'l satri ('*splat' — Express 5 yozuvi, "hamma yo'l"; Express 5 route sintaksisi) yoki { path, method }.
  • exclude(...) — istisnolar, forRoutes dan oldin yoziladi.

3.2 Demo: kim qachon ishladi

Demo loyihada VazifalarController da to'rtta marshrut: @Get(), @Post(), @All() (405 uchun, Controller'lar darsidagidek) va @Get('sogliq'). Har middleware har chaqirilganida hisoblagichga yozdi. Chiqishda har so'rov uchun ikki narsaga qarang: qaysi middleware'lar ishladi va necha marta (2 × belgisi) (biz ishga tushirdik: NestJS 12.2.0, Express 5.3.0, Node 24.21.0):

text
GET /vazifalar → 200 x-funksional=null
  2 × klass GET /vazifalar (soat-xizmati)
  1 × yolKor path=/ originalUrl=/vazifalar
POST /vazifalar → 201 x-funksional=ha
  2 × klass POST /vazifalar (soat-xizmati)
  1 × yolKor path=/ originalUrl=/vazifalar
  1 × funksional POST /vazifalar path=/vazifalar
GET /vazifalar/sogliq → 200 x-funksional=null
  1 × yolKor path=/ originalUrl=/vazifalar/sogliq
GET /yoq → 404 x-funksional=null
  1 × yolKor path=/ originalUrl=/yoq

Har qatorni o'qing:

  • DI ishladi — (soat-xizmati): klass middleware Soat xizmatini oldi.
  • { path, method } — funksional faqat POST /vazifalar da (x-funksional=ha), GET da — yo'q.
  • exclude — GET /vazifalar/sogliq da klass middleware ishlamadi.
  • /yoq — mavjud bo'lmagan yo'l: forRoutes(VazifalarController) dagi middleware ishlamadi. '*splat' dagisi esa ishladi.

Va ikkita g'alati narsa: klass middleware har so'rovda ikki marta (2 ×) ishladi, yolKor esa path=/ ni ko'rdi, garchi so'rov /vazifalar ga bo'lsa ham. Ikkalasi — keyingi bo'limda.

4. Ikki tuzoq

4.1 Ikki marta ishlash

forRoutes(VazifalarController) Nest'ga "shu controller'ning marshrutlari" deydi. Nest controller'ning har marshruti uchun middleware'ni alohida ro'yxatdan o'tkazadi — yo'li va metodi bilan. @Get(), @Post() va @All() ning yo'li bir xil (/vazifalar). GET /vazifalar so'roviga ikkita yozuv mos keladi: GET niki va ALL niki (@All — hamma metod). Har biri middleware'ni chaqirdi.

Buni brauzerda kichik modelda ko'raylik — har yozuv yo'l va metod bo'yicha moslanadi:

js
function ilova() {
  const qatlamlar = [];
  return {
    use(metod, yol, mw) { qatlamlar.push({ metod, yol, mw }); },
    sorov(metod, url) {
      const izlar = [];
      for (const q of qatlamlar) {
        const metodMos = q.metod === "ALL" || q.metod === metod;
        if (metodMos && url === q.yol) q.mw(`${metod} ${url}`, izlar);
      }
      return izlar;
    },
  };
}

const marshrutlar = [
  { metod: "GET", yol: "/vazifalar" },
  { metod: "POST", yol: "/vazifalar" },
  { metod: "ALL", yol: "/vazifalar" },
  { metod: "GET", yol: "/vazifalar/sogliq" },
];
const mw = (url, izlar) => izlar.push(`mw ${url}`);

// forRoutes(Controller): har marshrut uchun alohida yozuv
const a = ilova();
for (const m of marshrutlar) a.use(m.metod, m.yol, mw);
console.log(a.sorov("GET", "/vazifalar"));
console.log(a.sorov("POST", "/vazifalar"));
console.log(a.sorov("GET", "/vazifalar/sogliq"));

Konsolda:

text
[ 'mw GET /vazifalar', 'mw GET /vazifalar' ]
[ 'mw POST /vazifalar', 'mw POST /vazifalar' ]
[ 'mw GET /vazifalar/sogliq' ]

Model demo bilan bir xil natija berdi: GET va POST — ikki martadan (o'z yozuvi + ALL yozuvi), /sogliq — bir marta (u yo'lda @All yo'q). Model soddalashtirilgan: haqiqiy Express prefiks bo'yicha ham moslaydi va Nest bitta forRoutes ichidagi aynan bir xil yozuvlarni o'zi birlashtiradi. Lekin bir yo'ldagi turli metodli marshrutlar — alohida yozuvlar, va @All ularning har biriga "qo'shiladi".

Ko'p middleware uchun ikki marta ishlash zararsiz (log ikki qator, sarlavha qayta qo'yiladi). Lekin holatli middleware uchun — halokatli. Idempotentlik middleware'i birinchi chaqiruvda kalitni "jarayonda" deb yozadi. Ikkinchi chaqiruv o'sha kalitni ko'radi va o'ylaydi: "bu so'rov hali bajarilmoqda" — 409.

sequenceDiagram
  participant M as Mijoz
  participant A as idempotentlik (1)
  participant B as idempotentlik (2)
  participant O as Ombor
  M->>A: POST /v1/vazifalar, kalit K
  A->>O: boshla(K)
  O-->>A: "yangi" (K = jarayonda)
  A->>B: next()
  B->>O: boshla(K)
  O-->>B: "jarayonda"
  B-->>M: 409 So'rov bajarilmoqda

Mijoz birinchi urinishdayoq 409 oldi — vazifa yaratilmadi. Bu xatoni kanon testlari ushlagan. Biz uni qayta ko'rsatdik: «Loyiha qadami» dagi tuzatishni o'chirib, testlarni yurgizdik («Loyiha qadami» da chiqish).

Tuzatish — middleware o'zi "bu so'rovni ko'rdimmi?" ni eslab qoladi. So'rov obyekti — kalit, WeakSet — xotira: so'rov tugagach, yozuv o'z-o'zidan yo'qoladi.

4.2 req.path kesiladi

yolKor forRoutes('*splat') bilan ulangan va path=/ ni ko'rdi. Sabab — Express'ning app.use(yol, mw) qoidasi: middleware ichida req.path — ulangan yo'ldan keyingi qism (req obyekti). '*splat' butun yo'lni "yeb" qo'ydi va req.path ga / qoldi. req.originalUrl esa doim to'liq: /vazifalar/sogliq.

25-qismdagi eskirgan() aynan req.path dan foydalanardi — app.use(eskirgan(...)) (yo'lsiz) bilan bu to'g'ri edi. Nest middleware'iga ko'chganda u jim buzildi: Deprecation sarlavhalari chiqmay qoldi. Xato yo'q, 200 javob, faqat sarlavhalar yo'q — buni ham test ushladi.

Qoida: yo'l bilan ulangan middleware'da yo'lni req.originalUrl dan oling.

Tekshirib ko'ring: consumer.apply(logla).forRoutes("vazifalar") va middleware ichida console.log(req.path). GET /vazifalar/7 so'rovi nima chiqaradi?

Javob

/7. Middleware vazifalar yo'liga ulangan — Express shu qismni kesadi va req.path da qolgani qoladi. To'liq yo'l req.originalUrl da: /vazifalar/7 (query bo'lsa, u ham shu yerda — split("?")[0] bilan ajratiladi).

5. app.use() yoki modul middleware'i

app.use() (ilova.ts) modul configure()
Qachon ishlaydi Nest marshrutlaridan oldin, har so'rovda (404 ham) faqat forRoutes dagi marshrutlarda
DI yo'q (qo'lda uzatiladi) bor (klass middleware)
Yo'llar satr bilan, qo'lda controller yoki yo'l
Tartib yozilgan tartibda, modul middleware'laridan oldin modullar tartibida

Request lifecycle dagi kanon tajribasida tartib aynan shunday edi:

text
   1. middleware: global (app.use)
   2. middleware: modul (MiddlewareConsumer)
   3. guard: global (APP_GUARD)

Nima bo'ldi? Uchta qator — uchta qatlam. Avval app.use dagi middleware, keyin modul middleware'i, faqat shundan keyin guard. Ya'ni modul middleware'i guard'dan oldin ishlaydi: idempotentlik kalitini tekshirish ruxsat tekshiruvidan oldin bo'ladi. Hozircha bu muammo emas — loyihada auth yo'q. Lekin 28-qism auth'i Nest'ga ko'chganda aynan shu tartib haqiqiy xato beradi: idempotentlik guard'ning 401 javobini ham eslab qoladi. Uni Access/refresh token va cookie NestJS'da darsida topamiz va tuzatamiz.

Tanlov qoidasi:

  • Har so'rovga kerak (404 ham), DI kerak emas — app.use(): sorovId, so'rov logi, helmet, cors, statik fayllar, express.json, tanaXatosi. Masalan, 404 javobida ham X-Request-Id va xavfsizlik sarlavhalari bo'lishi kerak.
  • Ma'lum marshrutlarga yoki DI kerak — modul middleware'i: idempotentlik (faqat vazifalar to'plami, ombor DI'dan).

eskirgan() o'rtada: u hamma yo'lga ulanadi ('*splat'), lekin AppModule.configure da — ilova sozlamalari bir joyda (modulda) bo'lsin degan qaror bilan. app.use() da qolsa ham ishlardi.

5.1 Middleware nimani bilmaydi

Klass middleware DI oladi va Nest modulida yashaydi — lekin u baribir Express middleware'i. Unga (req, res, next) beriladi, boshqa hech narsa. Ikki narsa yo'q:

  • Qaysi handler ishlashi. Middleware ishlaganda Nest hali marshrutni handler'ga ulamagan. Shuning uchun @JsonTana() yoki @Rollar("admin") kabi belgilarni middleware o'qiy olmaydi — Reflector ga beradigan getHandler() yo'q.
  • Javobning natijasi. Middleware next() ni chaqiradi va handler qaytargan qiymatni ko'rmaydi. "Javobga ETag qo'sh" kabi ish — interceptor'niki (Interceptor'lar va RxJS).

Shu sababli Nest'da qoida: marshrutga bog'liq qaror (kirish mumkinmi, tana kerakmi) — guard'da, so'rovning umumiy tayyorgarligi (id, log, sarlavhalar, tana o'qish) — middleware'da. Idempotentlik o'rtada turadi: u marshrutga bog'liq, lekin handler belgisini o'qimaydi — faqat sarlavha va tanani. Shuning uchun u middleware bo'lib qoldi.

Tekshirib ko'ring: helmet() ni VazifalarModule.configure ichida forRoutes(VazifalarController) bilan ulasangiz, GET /yoq (404) javobida xavfsizlik sarlavhalari bo'ladimi?

Javob

Yo'q. forRoutes(VazifalarController) faqat shu controller marshrutlarida ishlaydi — demo chiqishidagi /yoq qatorida modul middleware'i ishlamagan edi. 404 javobi ham brauzerga boradi va himoya sarlavhalari unga ham kerak. Shuning uchun helmet — app.use() da, hamma so'rovga.

6. Hujumchi nigohi

  1. Middleware'ni chetlab o'tish. forRoutes("vazifalar") faqat shu yo'lni qamraydi; eskirgan versiyasiz yo'l, katta-kichik harf (/Vazifalar) yoki oxiridagi / boshqacha moslanishi mumkin. Agar middleware xavfsizlik ishini qilsa (masalan, imzo tekshiruvi) — chetlab o'tilgan yo'l himoyasiz. Himoya — xavfsizlik tekshiruvlari guard'da (marshrutga bog'langan, DI bilan) yoki app.use() da (hamma so'rovga); har yo'l varianti uchun test.
  2. Ikki marta ishlashni suiiste'mol qilish. Holatli middleware (limit hisoblagichi, idempotentlik) ikki marta ishlasa — limit ikki barobar tez tugaydi yoki 409 qaytadi. Hujumchi buni o'zi qila olmaydi, lekin xato hamma foydalanuvchiga ta'sir qiladi. Himoya — "bir so'rovga bir marta" himoyasi va test.
  3. req.path ga tayangan qaror. Yo'l bilan ulangan middleware'da req.path qisqa: if (req.path.startsWith("/admin")) tekshiruvi hech qachon ishlamasligi mumkin. Himoya — req.originalUrl.
  4. Idempotentlik kalitini tahmin qilish. Kalit faqat mijozga ma'lum bo'lishi kerak; tana izi (fingerprint) tekshiriladi — boshqa tana bilan bir xil kalit 422 (Idempotency-Key).

7. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Middleware ishlamaydi modul configure da ulanmagan yoki modul import qilinmagan implements NestModule + configure
Klass middleware'da bog'liqlik undefined @Injectable() yo'q yoki provider modulda emas @Injectable() + providers
Bir so'rovda ikki marta forRoutes(Controller), bir yo'lda bir nechta metod WeakSet<Request> himoyasi
404 da sarlavha yo'q middleware forRoutes(Controller) da app.use()
req.path — / yo'l bilan ulangan req.originalUrl
Parallel so'rovlarda qiymatlar aralashadi klass middleware maydonida so'rov holati holat req da yoki mahalliy o'zgaruvchida
TS2339: Property 'exclude' does not exist on type 'MiddlewareConsumer'. exclude forRoutes dan keyin yozilgan exclude(...).forRoutes(...)

Oxirgi qatordagi xatoni biz demo loyihada tsc bilan oldik (TypeScript 6.0.3). Tarjimasi: "MiddlewareConsumer turida exclude xususiyati yo'q". forRoutes zanjirni yopadi va yana consumer ni qaytaradi — unda faqat apply bor. Ya'ni bu xato jim o'tmaydi, kompilyator darhol ushlaydi. Tartib doim: apply → exclude → forRoutes.

8. Mashqlar

Mashqlar kurs/mashqlar/30/21-middleware/ da.

1-mashq (oson): to'ldiring

  • Klass middleware bajaradigan interfeys —
  • Middleware ulanadigan modul metodi —
  • Hamma yo'l (Express 5) — forRoutes("")
  • Istisno yo'l — .({ path: "vazifalar/sogliq", method: RequestMethod.GET })
  • Yo'l bilan ulangan middleware'da to'liq yo'l — req.
Yechim
  • NestMiddleware — use(req, res, next); DI uchun @Injectable().
  • configure — modul NestModule ni bajaradi, argument — MiddlewareConsumer.
  • *splat — Express 5 da nomli wildcard; eski * ishlamaydi.
  • exclude — forRoutes dan oldin.
  • originalUrl — req.path ulangan qismsiz.

2-mashq (o'rta): "bir so'rovga bir marta"

birMarta(mw) funksiyasini yozing: istalgan middleware'ni o'raydi va bitta so'rov obyekti uchun uni faqat birinchi chaqiruvda ishga tushiradi; keyingilarida darhol next(). So'rov tugagach xotira bo'shashi uchun WeakSet ishlating (WeakMap, WeakSet). Sinash uchun bitta so'rov obyektini ikki marta uzating.

Yechim
js
function birMarta(mw) {
  const korilgan = new WeakSet();
  return (req, res, next) => {
    if (korilgan.has(req)) {
      next();
      return;
    }
    korilgan.add(req);
    mw(req, res, next);
  };
}

let hisob = 0;
const sanovchi = (req, res, next) => {
  hisob++;
  next();
};
const himoyali = birMarta(sanovchi);

const req1 = { url: "/vazifalar" };
const req2 = { url: "/vazifalar" };
const next = () => {};
himoyali(req1, {}, next);
himoyali(req1, {}, next);
himoyali(req2, {}, next);
console.log(hisob);

Konsolda:

text
2

Uch chaqiruv, ikki hisob: req1 ikkinchi marta o'tkazib yuborildi, req2 — boshqa so'rov, u hisoblandi. Kanon IdempotentlikMiddleware aynan shu g'oyani ishlatadi. Set emas, WeakSet — chunki Set so'rov obyektlarini abadiy ushlab turardi va server xotirasi har so'rov bilan o'sardi.

3-mashq (qiyin): eskirgan modeli

25-qismdagi eskirgan(yollar) ni toza funksiya sifatida yozing: sarlavhalar(url, yollar) — url ning yo'l qismi (query'siz) eskirgan prefiks bilan boshlansa, { Deprecation, Link } qaytarsin, aks holda null. Link — voris yo'l + qolgan qism: /vazifalar/7 → </v1/vazifalar/7>; rel="successor-version". Deprecation — @1792800000. Prefiks moslashi aniq bo'lsin: /vazifalarim — mos emas. Ishora: startsWith, slice (Satr metodlari).

Yechim
js
const ESKI_YOLLAR = [
  { prefiks: "/vazifalar", voris: "/v1/vazifalar" },
  { prefiks: "/teglar", voris: "/v1/teglar" },
];

function sarlavhalar(url, yollar) {
  const yol = url.split("?")[0] ?? "";
  const y = yollar.find(
    (e) => yol === e.prefiks || yol.startsWith(e.prefiks + "/"),
  );
  if (y === undefined) return null;
  const qolgan = yol.slice(y.prefiks.length);
  return {
    Deprecation: "@1792800000",
    Link: `<${y.voris}${qolgan}>; rel="successor-version"`,
  };
}

const urllar = [
  "/vazifalar/7?x=1", "/teglar", "/vazifalarim", "/v1/vazifalar",
];
for (const url of urllar) {
  console.log(url, JSON.stringify(sarlavhalar(url, ESKI_YOLLAR)));
}

Konsolda:

text
/vazifalar/7?x=1 {"Deprecation":"@1792800000","Link":"</v1/vazifalar/7>; rel=\"successor-version\""}
/teglar {"Deprecation":"@1792800000","Link":"</v1/teglar>; rel=\"successor-version\""}
/vazifalarim null
/v1/vazifalar null

Funksiya url ni oladi — req.originalUrl ni. Agar unga req.path berilsa va middleware '*splat' bilan ulangan bo'lsa, url doim / bo'lardi va har qatorda null — darsdagi buzilishning aynan o'zi.

9. Loyiha qadami: idempotentlik va eskirgan — Nest middleware'lari

9.1 Nima o'zgaradi

Fayl O'zgarish
src/tokenlar.ts IDEMPOTENTLIK_OMBORI tokeni
src/bogliqliklar.module.ts ombor — useValue provider
src/umumiy/idempotentlik.middleware.ts yangi: klass middleware
src/vazifalar/vazifalar.module.ts configure — faqat vazifalar controller'iga
src/app.module.ts configure — eskirgan hamma yo'lga
src/umumiy/eskirgan.ts req.path → req.originalUrl
src/ilova.ts ikki app.use olib tashlanadi, ombor forRoot ga
test/marshrutlar.test.ts forRoot ga ombor

25-qismdagi idempotentlik.ts (middleware mantiqi) o'zgarmaydi — biz uni faqat Nest klassiga o'raymiz.

9.2 Token va provider

src/tokenlar.ts da IDEMPOTENTLIK_OMBORI tokeni bo'lishi kerak — bo'lmasa qo'shing:

ts
export const IDEMPOTENTLIK_OMBORI = Symbol("IDEMPOTENTLIK_OMBORI");

src/bogliqliklar.module.ts — tashqarida yaratilgan obyektlar DI'ga useValue bilan beriladi (Custom provider'lar). Ombor ham shular qatoriga qo'shiladi. Kanon faylidagi uzun qatorlarni bo'ldik (ma'no aynan):

ts
// bogliqliklar.module.ts — tashqarida yaratilgan obyektlar (log,
// xabarchi, config, idempotentlik ombori) DI konteyneriga
// beriladi: useValue provider'lar. @Global — har modul import
// qilmasdan ishlatadi; forRoot(...) — dinamik modul
import { Global, Module } from "@nestjs/common";
import type { DynamicModule, Provider } from "@nestjs/common";
import {
  CONFIG, IDEMPOTENTLIK_OMBORI, ILOVA_SOZLAMALARI, LOG, XABARCHI,
} from "./tokenlar.ts";
import type { IdempotentlikOmbori } from "./umumiy/idempotentlik.ts";
import type { IlovaSozlamalari } from "./tokenlar.ts";
import type { Config } from "./config.ts";
import type { Log } from "./umumiy/log.ts";
import type { Xabarchi } from "./umumiy/webhook.ts";

export type TashqiBogliqliklar = {
  log: Log;
  xabarchi?: Xabarchi | undefined;
  sozlamalar: IlovaSozlamalari;
  config?: Config | undefined;
  idempotentlikOmbori: IdempotentlikOmbori;
};

@Global()
@Module({})
export class BogliqliklarModule {
  static forRoot({
    log, xabarchi, sozlamalar, config, idempotentlikOmbori,
  }: TashqiBogliqliklar): DynamicModule {
    const providers: Provider[] = [
      { provide: LOG, useValue: log },
      // xabarchi ixtiyoriy: yo'q bo'lsa null (xizmat @Optional bilan)
      { provide: XABARCHI, useValue: xabarchi ?? null },
      { provide: ILOVA_SOZLAMALARI, useValue: sozlamalar },
      {
        provide: IDEMPOTENTLIK_OMBORI,
        useValue: idempotentlikOmbori,
      },
      ...(config === undefined
        ? [] : [{ provide: CONFIG, useValue: config }]),
    ];
    return {
      module: BogliqliklarModule,
      providers,
      exports: providers.map(
        (p) => (p as { provide: symbol }).provide,
      ),
    };
  }
}

9.3 src/umumiy/idempotentlik.middleware.ts

ts
// idempotentlik.middleware.ts — 25-qism Express middleware'i
// (idempotentlik.ts) Nest klass middleware'iga o'raladi: ombor
// DI'dan (IDEMPOTENTLIK_OMBORI tokeni — testda soxta soatli ombor,
// 27-qismda Redis), ulanish — VazifalarModule.configure()
import { Inject, Injectable } from "@nestjs/common";
import type { NestMiddleware } from "@nestjs/common";
import type {
  NextFunction, Request, RequestHandler, Response,
} from "express";
import { idempotentlik } from "./idempotentlik.ts";
import type { IdempotentlikOmbori } from "./idempotentlik.ts";
import { IDEMPOTENTLIK_OMBORI } from "../tokenlar.ts";

/**
 * Nest 12.2 TOPILMASI: forRoutes(VazifalarController) har marshrut
 * uchun alohida ro'yxatdan o'tkazadi; Express use() prefiks bo'yicha
 * moslaydi — POST /v1/vazifalar uchun middleware IKKI marta ishladi
 * ("" POST va @All "" marshrutlari) va ikkinchi chaqiruv
 * birinchisining "jarayonda" yozuvini ko'rib 409 qaytardi.
 * Shuning uchun: bir so'rovga bir marta
 */
const ishlangan = new WeakSet<Request>();

@Injectable()
export class IdempotentlikMiddleware implements NestMiddleware {
  readonly #ishlovchi: RequestHandler;

  constructor(
    @Inject(IDEMPOTENTLIK_OMBORI) ombor: IdempotentlikOmbori,
  ) {
    this.#ishlovchi = idempotentlik(ombor);
  }

  // Promise qaytariladi — Express 5 rad etilgan Promise'ni next(err)
  // ga yuboradi (409/422/400 → Problem Details filtri)
  use(req: Request, res: Response, next: NextFunction): unknown {
    if (ishlangan.has(req)) {
      next();
      return undefined;
    }
    ishlangan.add(req);
    return this.#ishlovchi(req, res, next);
  }
}
  • Constructor — @Inject(IDEMPOTENTLIK_OMBORI): IdempotentlikOmbori — interfeys, ish vaqtida yo'q, shuning uchun token (DI va IoC konteyner). 25-qismdagi idempotentlik(ombor) bir marta chaqiriladi va tayyor Express middleware'i #ishlovchi da saqlanadi.
  • ishlangan — modul darajasidagi WeakSet: «Bir so'rovga bir marta» mashqidagi naqsh.
  • return this.#ishlovchi(...) — 25-qism middleware'i async; xato tashlasa (409, 422, 400) rad etilgan Promise qaytadi. Express 5 uni ushlab next(err) qiladi (Express 5: async xatolar) va xato filtrga yetadi.

9.4 Ulash

src/vazifalar/vazifalar.module.ts:

ts
// vazifalar.module.ts — vazifalar xususiyat moduli: controller'lar
// + xizmat. configure() — Idempotency-Key middleware'i FAQAT shu
// controller marshrutlariga (25 dagi v1.use("/vazifalar", …))
import { Module } from "@nestjs/common";
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
import { VazifalarController } from "./vazifalar.controller.ts";
import { TeglarController } from "./teglar.controller.ts";
import { VazifalarService } from "./vazifalar.service.ts";
import {
  IdempotentlikMiddleware,
} from "../umumiy/idempotentlik.middleware.ts";

@Module({
  controllers: [VazifalarController, TeglarController],
  providers: [VazifalarService],
  // boshqa modul (masalan eksport) xizmatdan foydalanmoqchi bo'lsa
  exports: [VazifalarService],
})
export class VazifalarModule implements NestModule {
  configure(consumer: MiddlewareConsumer): void {
    consumer.apply(IdempotentlikMiddleware)
      .forRoutes(VazifalarController);
  }
}

forRoutes(VazifalarController) — TeglarController emas: 25-qismda ham idempotentlik faqat vazifalar to'plamida edi. Versiyali (/v1/vazifalar) va eskirgan (/vazifalar) yo'llar ikkalasi ham qamraladi — controller ikkala yo'lda ro'yxatdan o'tgan.

src/app.module.ts — AppModule ham NestModule bo'ladi (importlarga MiddlewareConsumer, NestModule, eskirgan, ESKI_YOLLAR qo'shiladi):

ts
@Module({})
export class AppModule implements NestModule {
  static forRoot(/* … o'zgarishsiz … */) { /* … */ }

  // Funksional middleware: eski versiyasiz yo'llar ishlaydi, lekin
  // Deprecation + Sunset + voris havolasi bilan — hamma yo'lga
  // ("*splat" — Express 5 sintaksisi; eskirgan() o'zi /vazifalar,
  // /teglar prefiksini tekshiradi)
  configure(consumer: MiddlewareConsumer): void {
    consumer.apply(eskirgan(ESKI_YOLLAR)).forRoutes("*splat");
  }
}

9.5 eskirgan.ts va ilova.ts

src/umumiy/eskirgan.ts da bitta qaror o'zgaradi — yo'l req.originalUrl dan:

diff
 export function eskirgan(
   yollar: readonly EskiYol[],
 ): RequestHandler {
   return (req, res, next) => {
-    const y = yollar.find((e) => req.path === e.prefiks
-      || req.path.startsWith(e.prefiks + "/"));
+    // req.path EMAS: yo'l bilan ulanganda (forRoutes("*splat"))
+    // Express ulangan qismni kesadi — req.path "/" bo'ladi
+    const yol = req.originalUrl.split("?")[0] ?? "";
+    const y = yollar.find((e) => yol === e.prefiks
+      || yol.startsWith(e.prefiks + "/"));
     if (y !== undefined) {
       …
-      res.append("Link",
-        `<${y.voris}${req.path.slice(y.prefiks.length)}>; …`);
+      res.append("Link",
+        `<${y.voris}${yol.slice(y.prefiks.length)}>; …`);

src/ilova.ts da ikki app.use va ikki import o'chadi, ombor forRoot ga uzatiladi:

diff
-import { idempotentlik, XotiraIdempotentlikOmbori }
-  from "./umumiy/idempotentlik.ts";
+import { XotiraIdempotentlikOmbori }
+  from "./umumiy/idempotentlik.ts";
-import { eskirgan, ESKI_YOLLAR } from "./umumiy/eskirgan.ts";
 …
-  AppModule.forRoot({
-    ombor, config, log, xabarchi, sozlamalar: { tz },
-  })
+  AppModule.forRoot({
+    ombor, config, log, xabarchi, idempotentlikOmbori,
+    sozlamalar: { tz },
+  })
 …
-  app.use(eskirgan(ESKI_YOLLAR));
-  app.use(["/v1/vazifalar", "/vazifalar"],
-    idempotentlik(idempotentlikOmbori));
+  // Idempotency-Key va eskirgan yo'l sarlavhalari — endi Nest
+  // middleware'lari (modullarning configure()): DI bilan

idempotentlikOmbori — ilovaYarat parametri (sukut — new XotiraIdempotentlikOmbori()); testlar o'z soxta soatli omborini beradi. test/marshrutlar.test.ts AppModule.forRoot ni to'g'ridan chaqiradi — endi ombor majburiy:

diff
+import { XotiraIdempotentlikOmbori }
+  from "../src/umumiy/idempotentlik.ts";
 …
   AppModule.forRoot({
     ombor: new XotiraOmbori(), log: logYarat("silent"),
+    idempotentlikOmbori: new XotiraIdempotentlikOmbori(),
     sozlamalar: { tz: "Asia/Tashkent" },
   })

9.6 Tekshiruv — va tuzatishlarsiz nima bo'lardi

npm run tip, npm run lint va npm test toza o'tishi kerak (Postgres'siz yurgizsangiz: Tests 75 passed | 6 skipped (81) — bazali testlar SKIP).

Endi ikki tuzatishning qiymatini ko'raylik. Kanonning shu bosqichdagi nusxasida WeakSet tekshiruvini o'chirdik va testlarni yurgizdik (biz ishga tushirdik, Vitest 5.0.5, Postgres'siz; chiqishni qisqartirdik):

text
 ❯ test/mock.test.ts (5 tests | 1 failed) 1812ms
   × stub (vi.spyOn): ombor.yarat bir marta chaqirildi — Idempotency-Key takrorida ombor CHAQIRILMAYDI 111ms
 ❯ test/idempotentlik.test.ts (5 tests | 4 failed) 1832ms
   × takroriy POST bir xil kalit bilan — bitta vazifa, ikkinchisi saqlangan javob (Idempotency-Replayed) 547ms
   × parallel: bir xil kalit — biri 201, ikkinchisi 409 + Retry-After 57ms
   × xato javobi (422) ham saqlanadi; muddati o'tgach kalit qayta ishlaydi 16ms
   × regressiya (…): mijoz kutmay uzilsa ham qayta urinish ikkinchi vazifa yaratmaydi 20ms

 FAIL  test/idempotentlik.test.ts > takroriy POST bir xil kalit bilan — bitta vazifa, ikkinchisi saqlangan javob (Idempotency-Replayed)
AssertionError: Expected values to be strictly equal:

409 !== 201

 Test Files  2 failed | 16 passed | 5 skipped (23)
      Tests  5 failed | 70 passed | 6 skipped (81)

Tarjimasi: "kutilgan 201, keldi 409". Birinchi POST — kalit bilan, birinchi marta — 409 oldi: «Ikki marta ishlash» bo'limidagi diagramma aynan. Parallel testda ikkala so'rov 409 oldi, vazifa umuman yaratilmadi.

Keyin WeakSet ni qaytarib, eskirgan.ts da originalUrl o'rniga yana req.path qo'ydik:

text
 ❯ test/shartli.test.ts (3 tests | 1 failed) 2445ms
   × versiyasiz eski yo'l ishlaydi, lekin Deprecation, Sunset va voris havolasi bilan 66ms

 FAIL  test/shartli.test.ts > versiyasiz eski yo'l ishlaydi, lekin Deprecation, Sunset va voris havolasi bilan
AssertionError: Expected values to be strictly equal:
+ actual - expected

+ null
- '@1792800000'

 ❯ test/shartli.test.ts:52:10
     50|   const r = await fetch(`${asos}/vazifalar/1`);
     51|   assert.equal(r.status, 200);
     52|   assert.equal(r.headers.get("deprecation"), "@1792800000");

r.status — 200 (oldingi qator o'tdi), deprecation sarlavhasi — null. Mijoz "hammasi joyida" deb o'ylaydi, eskirgan yo'ldan ko'chish kerakligini bilmaydi.

Ikkala xato ham 25–27-qismlarning o'zgarmagan testlari bilan ushlandi: Express'dan Nest'ga ko'chishda testlar — xulq o'zgarmaganining yagona kafolati.

Hali commit qilmang — loyihaning bu bosqichi Global enhancer'lar va bajarilish tartibi darsida commit bilan yopiladi.

10. Real ishda

  • Nest loyihalarida klass middleware ko'pincha log, so'rov konteksti, tenant aniqlash uchun; xavfsizlik tekshiruvlari esa guard'da. "Middleware'da auth" — Express'dan ko'chgan loyihalarning belgisi.
  • Ikki marta ishlash tuzog'i haqida Nest hujjatida ogohlantirish yo'q (2026-11 holatida) — uni bilish va test bilan himoyalanish sizning ishingiz.
  • Express'dan ko'chirishda eng xavfsiz yo'l — avval hamma middleware'ni app.use() da o'zgarishsiz qoldirish, keyin bittalab modulga o'tkazish va har qadamda eski testlarni yurgizish. Biz ham shunday qildik: ikki tuzoqni aynan o'sha testlar ushladi.
  • Intervyu: "Middleware handler'ni biladimi?" (yo'q — shuning uchun rol tekshiruvi guard'da), "Nest'da middleware va guard farqi?", "middleware'ga DI qanday beriladi?", "forRoutes ga nima berish mumkin?".

Xulosa

  • Nest middleware — Express middleware'i: funksional ((req, res, next)) yoki klass (NestMiddleware.use, DI bilan).
  • Ulash — NestModule.configure(consumer): apply(...).exclude(...).forRoutes(...); controller, yo'l yoki { path, method }.
  • app.use() — har so'rov (404 ham) va modul middleware'laridan oldin; modul middleware — faqat o'z marshrutlariga.
  • Tuzoqlar: forRoutes(Controller) — bir yo'lda bir nechta metod bo'lsa middleware ikki marta (WeakSet himoyasi); yo'l bilan ulanganda req.path kesiladi (req.originalUrl).
  • Middleware handler'ni ham, uning natijasini ham bilmaydi: marshrut belgisiga bog'liq qaror — guard'da, javobni o'zgartirish — interceptor'da.
  • vazifalar-api: idempotentlik — VazifalarModule da DI bilan, eskirgan — AppModule.configure da.

Keyingi dars: Guard'lar — CanActivate, ExecutionContext, true/false va xato tashlash farqi; JsonTanaGuard ning ichini va Reflector.createDecorator lint topilmasini ko'ramiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Middleware Nest'da: NestMiddleware, MiddlewareConsumer, forRoutes va ikki marta ishlash tuzog'i — IlmHamroh