Xây dựng Module “Report” có Filtering, Pagination và Caching trong NestJS

3 min read

Giới thiệu

Khi phát triển hệ thống backend, module báo cáo (report) là một trong những phần quan trọng nhất — thường chứa các truy vấn dữ liệu phức tạp, yêu cầu phân trang, lọc theo nhiều tiêu chí và xử lý nhanh.
Vì vậy, nếu không tối ưu đúng cách, API report dễ trở thành “nút thắt cổ chai” khiến toàn bộ hệ thống chậm đi đáng kể.

Trong bài viết này, bạn sẽ học cách:

  • Xây dựng module Report chuẩn cấu trúc NestJS.
  • Áp dụng filteringpagination đúng cách.
  • Tăng tốc API bằng Redis caching.
  • Xử lý lỗi và validate dữ liệu đầu vào chuẩn REST.

🧩 Cấu trúc module Report theo Clean Architecture

src/
 └── modules/
     └── report/
         ├── controllers/
         │   └── report.controller.ts
         ├── services/
         │   └── report.service.ts
         ├── repositories/
         │   └── report.repository.ts
         ├── dtos/
         │   └── report-filter.dto.ts
         └── entities/
             └── report.entity.ts

Giải thích:

  • report.controller.ts: Xử lý request & response.
  • report.service.ts: Chứa logic nghiệp vụ.
  • report.repository.ts: Truy xuất dữ liệu từ DB.
  • report-filter.dto.ts: Validate các tham số lọc & phân trang.
  • report.entity.ts: Định nghĩa model dữ liệu (ORM).

⚙️ Bước 1 – Tạo DTO Filtering và Pagination

// report-filter.dto.ts
import { Type } from 'class-transformer';
import { IsOptional, IsInt, IsString, Min } from 'class-validator';

export class ReportFilterDto {
  @IsOptional()
  @IsString()
  user?: string;

  @IsOptional()
  @IsString()
  category?: string;

  @IsOptional()
  @Type(() => Date)
  startDate?: Date;

  @IsOptional()
  @Type(() => Date)
  endDate?: Date;

  @IsOptional()
  @Type(() => Number)
  @IsInt()
  @Min(1)
  page = 1;

  @IsOptional()
  @Type(() => Number)
  @IsInt()
  @Min(1)
  limit = 10;
}

Việc dùng class-transformer + class-validator giúp dữ liệu query tự động được validate và convert sang đúng kiểu.


🧱 Bước 2 – Repository: Lọc và phân trang dữ liệu

// report.repository.ts
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Report } from '../entities/report.entity';
import { ReportFilterDto } from '../dtos/report-filter.dto';

@Injectable()
export class ReportRepository {
  constructor(
    @InjectRepository(Report)
    private readonly repo: Repository<Report>,
  ) {}

  async findReports(filter: ReportFilterDto) {
    const { user, category, startDate, endDate, page, limit } = filter;
    const query = this.repo.createQueryBuilder('report');

    if (user) query.andWhere('report.user = :user', { user });
    if (category) query.andWhere('report.category = :category', { category });
    if (startDate) query.andWhere('report.date >= :startDate', { startDate });
    if (endDate) query.andWhere('report.date <= :endDate', { endDate });

    const [data, total] = await query
      .orderBy('report.date', 'DESC')
      .skip((page - 1) * limit)
      .take(limit)
      .getManyAndCount();

    return { data, total, page, limit };
  }
}

🚀 Bước 3 – Service: Thêm cache để tăng tốc

// report.service.ts
import { Injectable } from '@nestjs/common';
import { Cache } from 'cache-manager';
import { Inject } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { ReportRepository } from '../repositories/report.repository';
import { ReportFilterDto } from '../dtos/report-filter.dto';

@Injectable()
export class ReportService {
  constructor(
    private readonly reportRepo: ReportRepository,
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
  ) {}

  async getReports(filter: ReportFilterDto) {
    const cacheKey = `reports:${JSON.stringify(filter)}`;
    const cached = await this.cacheManager.get(cacheKey);
    if (cached) return cached;

    const result = await this.reportRepo.findReports(filter);
    await this.cacheManager.set(cacheKey, result, 60); // TTL 60s
    return result;
  }
}

Lần gọi đầu tiên sẽ truy vấn DB, những lần sau trong 60 giây sẽ lấy kết quả trực tiếp từ cache.


🧠 Bước 4 – Controller: Xử lý request và trả kết quả

// report.controller.ts
import { Controller, Get, Query, UseInterceptors } from '@nestjs/common';
import { ReportService } from '../services/report.service';
import { ReportFilterDto } from '../dtos/report-filter.dto';
import { CacheInterceptor } from '@nestjs/cache-manager';

@UseInterceptors(CacheInterceptor)
@Controller('reports')
export class ReportController {
  constructor(private readonly reportService: ReportService) {}

  @Get()
  async getReports(@Query() filter: ReportFilterDto) {
    return this.reportService.getReports(filter);
  }
}

✅ API /reports?user=john&category=sales&page=1&limit=10
→ trả về dữ liệu đã phân trang và cache sẵn.


📈 Ví dụ phản hồi JSON

{
  "page": 1,
  "limit": 10,
  "total": 245,
  "data": [
    { "id": 1, "user": "john", "category": "sales", "date": "2025-09-25", "amount": 12000 },
    ...
  ]
}

💡 Mẹo tối ưu nâng cao

  • Dùng TTL khác nhau cho từng filter (reports:daily, reports:monthly).
  • Kết hợp Interceptor để log thời gian phản hồi.
  • Tích hợp Exception Filter để hiển thị lỗi thống nhất.
  • Nếu dữ liệu thay đổi thường xuyên, có thể reset cache theo key khi ghi dữ liệu mới.

✅ Ưu điểm khi áp dụng Filtering, Pagination, Caching

Lợi íchMô tả
Truy vấn nhanh hơnGiảm 50–80% thời gian phản hồi nhờ cache.
Dễ mở rộngChỉ cần thêm điều kiện filter mới.
Bảo trì dễ dàngRepository tách biệt logic DB.
Trải nghiệm mượt màAPI phân trang trả kết quả gọn nhẹ.

⚠️ Lỗi thường gặp

LỗiNguyên nhânCách khắc phục
Kết quả cache không cập nhậtTTL quá dàiGiảm TTL hoặc xóa cache sau khi ghi dữ liệu
Phân trang saiKhông convert sốDùng class-transformer để ép kiểu
Lỗi 500Không xử lý ngoại lệThêm GlobalExceptionFilter
Dữ liệu trùng key cacheKhông tạo cache key riêngDùng JSON.stringify(filter) để tạo key

🔗 Tham khảo thêm


🚀 Kết luận

Module Report là minh chứng rõ nhất cho sức mạnh của kiến trúc tách lớp trong NestJS.
Bằng cách kết hợp Filtering + Pagination + Caching, bạn có thể xử lý lượng dữ liệu lớn mà vẫn đảm bảo hiệu năng cao.
Hơn nữa, nhờ sử dụng Repository Pattern, Validation, và Exception Filter, mã nguồn của bạn luôn rõ ràng, dễ bảo trì và sẵn sàng mở rộng trong tương lai.

⚡ Kết hợp tất cả kỹ thuật trong 5 bài viết này, bạn đã nắm vững nền tảng để xây dựng hệ thống backend NestJS chuẩn Clean Architecture.

Avatar photo

Leave a Reply

Your email address will not be published. Required fields are marked *