Interceptors — Response Mapping, Logging

~20 daq · NestJS · Boshlang'ich daraja

Interceptor nima?

Hozirgacha o'rgangan narsalarimizni eslaylik:

  • Middleware — so'rov kelganida ishga tushadi, faqat so'rovni ko'radi
  • Guard — foydalanuvchining ruxsatini tekshiradi
  • Pipe — kelgan ma'lumotni tekshiradi va o'zgartiradi

Interceptor esa bulardan farqli — u ikki tomonda ham ishlaydi. Ham so'rov kelayotganda, ham javob ketayotganda. Bu unga boshqalarga qilib bo'lmaydigan narsalarni qilish imkonini beradi.

Hayotiy misol: siz telefon qo'ng'iroq qilyapsiz. O'rtada tarjimon o'tiribdi. U sizning gapingizni eshitadi, tarjima qiladi, yuboradi. Javob kelganda ham xuddi shunday — tarjima qilib sizga yetkazadi. Interceptor — shu tarjimon.

So'rov keldi
        ↓
[ Interceptor ] ← so'rovni ko'radi, o'zgartirishi mumkin
        ↓
[ Controller ]
        ↓
[ Service ]
        ↓
[ Interceptor ] ← javobni ko'radi, o'zgartirishi mumkin
        ↓
Javob ketdi

Interceptor ning tuzilishi

Interceptor NestInterceptor interfeysini implement qiladi. Bu interfeys bitta metod talab qiladi — intercept:

// common/interceptors/logging.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Logger,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger("Interceptor");

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const { method, url } = request;
    const start = Date.now();

    this.logger.log(`→ ${method} ${url} — so'rov keldi`);

    // next.handle() — keyingi bosqichga o'tkazadi (Controller ga)
    // pipe() — javob qaytganda nima qilishni belgilaydi
    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - start;
        this.logger.log(`← ${method} ${url} — ${duration}ms da tugadi`);
      }),
    );
  }
}

Bu yerda ikki yangi tushuncha bor — CallHandler va Observable. Ularni tushunish muhim.

CallHandler — bu "keyingi bosqich" ni boshqaruvchi ob'ekt. next.handle() chaqirilsa — so'rov Controller ga o'tadi va u yerdan javob qaytadi. Chaqirilmasa — Controller hech qachon ishlamaydi.

Observable — bu RxJS kutubxonasidan. NestJS da javob Observable sifatida qaytadi. Uni pipe() va turli operatorlar bilan o'zgartirish mumkin. Hozircha shuni biling: next.handle() — Controller ning javobi, .pipe() — o'sha javob bilan nima qilish.

tap() — javobni o'zgartirmaydi, faqat "ko'radi". Loglash, statistika yig'ish uchun ishlatiladi.


Response Mapping — javobni o'zgartirish

Haqiqiy loyihalarda barcha API javoblari bir xil formatda bo'lishi kerak. Masalan:

{
  "success": true,
  "data": { ... },
  "timestamp": "2024-01-15T10:30:00.000Z"
}

Lekin Controller shunchaki ma'lumotni qaytaradi:

@Get()
findAll() {
  return [{ id: 1, name: 'Jasur' }];  // faqat ma'lumot
}

Har bir Controller da bu formatni qo'lda yozish — juda ko'p takrorlanish. Interceptor buni bir joyda hal qiladi:

// common/interceptors/transform.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      // map() — javobni o'zgartiradi
      map((data) => ({
        success: true,
        data,
        timestamp: new Date().toISOString(),
      })),
    );
  }
}

map() — tap() dan farqli. U javobni o'zgartiradi. Controller [{ id: 1, name: 'Jasur' }] qaytargan bo'lsa — map() uni yangi formatga o'raydi.

Endi foydalanuvchi quyidagini ko'radi:

{
  "success": true,
  "data": [{ "id": 1, "name": "Jasur" }],
  "timestamp": "2024-01-15T10:30:00.000Z"
}

Controller da hech narsa o'zgarmadi. Interceptor hamma ishni qildi.


Interceptor ni ulash

Xuddi Guard kabi — uch joyda ulanishi mumkin.

Global darajasida — butun ilova uchun, main.ts da:

// main.ts
import { TransformInterceptor } from "./common/interceptors/transform.interceptor";
import { LoggingInterceptor } from "./common/interceptors/logging.interceptor";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalInterceptors(
    new LoggingInterceptor(),
    new TransformInterceptor(),
  );

  await app.listen(3000);
}

Controller darajasida:

import { Controller, UseInterceptors } from "@nestjs/common";
import { TransformInterceptor } from "../common/interceptors/transform.interceptor";

@Controller("users")
@UseInterceptors(TransformInterceptor)
export class UsersController {}

Metod darajasida:

import { Get, UseInterceptors } from "@nestjs/common";
import { LoggingInterceptor } from "../common/interceptors/logging.interceptor";

@Get()
@UseInterceptors(LoggingInterceptor)
findAll() {}

Real loyihalarda TransformInterceptor va LoggingInterceptor ni global qo'yish eng to'g'ri yondashuv. Barcha javoblar bir xil formatda bo'ladi, barcha so'rovlar loglanadi.


Exception Mapping — xatolarni ham formatlash

Oldingi darsda ExceptionFilter yozdik — xatolarni ushlash uchun. Interceptor ham xatolarni ushlashi mumkin. Farqi: Interceptor javobni ham, xatoni ham bitta joyda boshqaradi.

// common/interceptors/error.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  HttpException,
  HttpStatus,
} from "@nestjs/common";
import { Observable, throwError } from "rxjs";
import { catchError } from "rxjs/operators";

@Injectable()
export class ErrorInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      catchError((error) => {
        // HttpException bo'lsa — uning ma'lumotini olamiz
        if (error instanceof HttpException) {
          return throwError(() => error);
        }

        // Kutilmagan xato bo'lsa — 500 qaytaramiz
        return throwError(
          () =>
            new HttpException(
              {
                success: false,
                message: "Ichki server xatosi",
                statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
              },
              HttpStatus.INTERNAL_SERVER_ERROR,
            ),
        );
      }),
    );
  }
}

catchError() — javob o'rniga xato kelsa ishga tushadi. throwError() — xatoni qayta otadi, lekin endi formatlangan holda.


Amaliy misol — so'rov vaqtini o'lchash

Real loyihalarda ba'zi endpointlar sekin ishlaydi. Qaysi endpoint qancha vaqt olayotganini bilish uchun:

// common/interceptors/performance.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Logger,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";

@Injectable()
export class PerformanceInterceptor implements NestInterceptor {
  private readonly logger = new Logger("Performance");
  private readonly SLOW_REQUEST_THRESHOLD = 1000; // 1 sekund

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const { method, url } = request;
    const start = Date.now();

    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - start;

        // 1 sekunddan ko'p vaqt olsa — ogohlantirish
        if (duration > this.SLOW_REQUEST_THRESHOLD) {
          this.logger.warn(`⚠️ Sekin so'rov: ${method} ${url} — ${duration}ms`);
        } else {
          this.logger.log(`✓ ${method} ${url} — ${duration}ms`);
        }
      }),
    );
  }
}

Caching Interceptor — keshlashtirish

Ba'zi ma'lumotlar tez-tez o'zgarmaydi — masalan, kategoriyalar ro'yxati, davlatlar ro'yxati. Har safar ma'lumotlar bazasiga murojaat qilish — resurs isrof. Kesh — bir marta olasiz, vaqtincha saqlab qo'yasiz:

// common/interceptors/cache.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from "@nestjs/common";
import { Observable, of } from "rxjs";
import { tap } from "rxjs/operators";

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  // Oddiy xotira keshi — real loyihalarda Redis ishlatiladi
  private cache = new Map<string, { data: any; expiry: number }>();
  private readonly TTL = 60 * 1000; // 1 daqiqa

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // Faqat GET so'rovlarini keshamiz
    const request = context.switchToHttp().getRequest();
    if (request.method !== "GET") {
      return next.handle();
    }

    const cacheKey = request.url;
    const cached = this.cache.get(cacheKey);

    // Keshda bor va muddati o'tmagan bo'lsa — keshdan qaytaramiz
    if (cached && cached.expiry > Date.now()) {
      return of(cached.data);
    }

    // Keshda yo'q — Controller dan olamiz va keshamiz
    return next.handle().pipe(
      tap((data) => {
        this.cache.set(cacheKey, {
          data,
          expiry: Date.now() + this.TTL,
        });
      }),
    );
  }
}

of(cached.data) — bu RxJS dan. Mavjud qiymatdan Observable yaratadi. Ya'ni Controller ga umuman murojaat qilmasdan — keshdan javob qaytaradi.


Interceptor lar ketma-ketligi

Bir nechta Interceptor bo'lganda ular ichma-ich ishlaydi — xuddi piyoz qobiqlari kabi:

So'rov  → LoggingInterceptor → TransformInterceptor → Controller
Javob   ← LoggingInterceptor ← TransformInterceptor ← Controller

TransformInterceptor javobni formatlaydi. Keyin LoggingInterceptor o'sha formatlangan javobni loglaydi. Tartib muhim — main.ts da qaysi birinchi yozilsa, so'rovda birinchi, javobda oxirgi ishlaydi.


Xulosa

Interceptor — NestJS da eng kuchli mexanizmlardan biri. U so'rov va javobning ikki tomonida ham turadi. Bir martalik sozlama bilan butun ilovaning javob formatini, loglashini, keshini boshqarish mumkin.

Eslab qoling:

  • NestInterceptor — interfeys, intercept metodini yozish shart
  • next.handle() — Controller ga o'tkazadi, Observable qaytaradi
  • tap() — javobni o'zgartirmaydi, faqat ko'radi
  • map() — javobni o'zgartiradi
  • catchError() — xatolarni ushlaydi
  • of() — mavjud qiymatdan Observable yaratadi
  • Global Interceptor — main.ts da bir marta, hamma joyga ta'sir qiladi
  • Interceptor lar ichma-ich ishlaydi — tartib muhim

Keyingi dars

11-dars: Custom Decorators