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 filtering và pagination đú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 ích | Mô tả |
---|---|
Truy vấn nhanh hơn | Giảm 50–80% thời gian phản hồi nhờ cache. |
Dễ mở rộng | Chỉ cần thêm điều kiện filter mới. |
Bảo trì dễ dàng | Repository 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ỗi | Nguyên nhân | Cách khắc phục |
---|---|---|
Kết quả cache không cập nhật | TTL quá dài | Giảm TTL hoặc xóa cache sau khi ghi dữ liệu |
Phân trang sai | Không convert số | Dùng class-transformer để ép kiểu |
Lỗi 500 | Không xử lý ngoại lệ | Thêm GlobalExceptionFilter |
Dữ liệu trùng key cache | Không tạo cache key riêng | Dù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.