260223:1415 20260223 nextJS & nestJS Best pratices
Build and Deploy / deploy (push) Successful in 4m44s

This commit is contained in:
admin
2026-02-23 14:15:06 +07:00
parent c90a664f53
commit ef16817f38
164 changed files with 24815 additions and 311 deletions
@@ -1,5 +1,5 @@
// File: src/common/exceptions/http-exception.filter.ts
// บันทึกการแก้ไข: ปรับปรุง Global Filter ให้จัดการ Error ปลอดภัยสำหรับ Production และ Log ละเอียดใน Dev (T1.1)
// Fix #3 & #4: แทน console.error ด้วย Logger, เพิ่ม ErrorResponseBody interface
import {
ExceptionFilter,
@@ -11,6 +11,16 @@ import {
} from '@nestjs/common';
import { Request, Response } from 'express';
interface ErrorResponseBody {
statusCode: number;
timestamp: string;
path: string;
message?: unknown;
error?: string;
stack?: string;
[key: string]: unknown;
}
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name);
@@ -33,49 +43,46 @@ export class HttpExceptionFilter implements ExceptionFilter {
: { message: 'Internal server error' };
// จัดรูปแบบ Error Message ให้เป็น Object เสมอ
let errorBody: any =
let errorBody: Record<string, unknown> =
typeof exceptionResponse === 'string'
? { message: exceptionResponse }
: exceptionResponse;
: (exceptionResponse as Record<string, unknown>);
// 3. 📝 Logging Strategy (แยกตามความรุนแรง)
if (status >= 500) {
// 💥 Critical Error: Log stack trace เต็มๆ
this.logger.error(
`💥 HTTP ${status} Error on ${request.method} ${request.url}`,
exception instanceof Error
? exception.stack
: JSON.stringify(exception),
`HTTP ${status} Error on ${request.method} ${request.url}`,
exception instanceof Error ? exception.stack : JSON.stringify(exception)
);
// 👇👇 สิ่งที่คุณต้องการ: Log ดิบๆ ให้เห็นชัดใน Docker Console 👇👇
console.error('💥 REAL CRITICAL ERROR:', exception);
} else {
// ⚠️ Client Error (400, 401, 403, 404): Log แค่ Warning พอ ไม่ต้อง Stack Trace
this.logger.warn(
`⚠️ HTTP ${status} Error on ${request.method} ${request.url}: ${JSON.stringify(errorBody.message || errorBody)}`,
`HTTP ${status} Error on ${request.method} ${request.url}: ${JSON.stringify(errorBody['message'] ?? errorBody)}`
);
}
// 4. 🔒 Security & Response Formatting
// กรณี Production และเป็น Error 500 -> ต้องซ่อนรายละเอียดความผิดพลาดของ Server
if (status === 500 && process.env.NODE_ENV === 'production') {
if (status === 500 && process.env['NODE_ENV'] === 'production') {
errorBody = {
message: 'Internal server error',
// อาจเพิ่ม reference code เพื่อให้ user แจ้ง support ได้ เช่น code: 'ERR-500'
};
}
// 5. Construct Final Response
const responseBody = {
// 5. Construct Final Response (type-safe)
const responseBody: ErrorResponseBody = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
...errorBody, // Spread message, error, validation details
...errorBody,
};
// 🛠️ Development Mode: แถม Stack Trace ไปให้ Frontend Debug ง่ายขึ้น
if (process.env.NODE_ENV !== 'production' && exception instanceof Error) {
if (
process.env['NODE_ENV'] !== 'production' &&
exception instanceof Error
) {
responseBody.stack = exception.stack;
}
@@ -1,3 +1,7 @@
// File: src/common/interceptors/audit-log.interceptor.ts
// Fix #2: Replaced `as unknown as AuditLog` with CreateAuditLogPayload typed interface
// Lint fixes: async-in-tap pattern, null vs undefined entityId, safe any handling
import {
CallHandler,
ExecutionContext,
@@ -16,6 +20,17 @@ import { AuditLog } from '../entities/audit-log.entity';
import { AUDIT_KEY, AuditMetadata } from '../decorators/audit.decorator';
import { User } from '../../modules/user/entities/user.entity';
/** Typed payload for creating AuditLog, replacing the `as unknown as AuditLog` cast */
interface CreateAuditLogPayload {
userId: number | null | undefined;
action: string;
entityType?: string;
entityId?: string;
ipAddress?: string;
userAgent?: string;
severity: string;
}
@Injectable()
export class AuditLogInterceptor implements NestInterceptor {
private readonly logger = new Logger(AuditLogInterceptor.name);
@@ -23,13 +38,13 @@ export class AuditLogInterceptor implements NestInterceptor {
constructor(
private reflector: Reflector,
@InjectRepository(AuditLog)
private auditLogRepo: Repository<AuditLog>,
private auditLogRepo: Repository<AuditLog>
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const auditMetadata = this.reflector.getAllAndOverride<AuditMetadata>(
AUDIT_KEY,
[context.getHandler(), context.getClass()],
[context.getHandler(), context.getClass()]
);
if (!auditMetadata) {
@@ -37,44 +52,70 @@ export class AuditLogInterceptor implements NestInterceptor {
}
const request = context.switchToHttp().getRequest<Request>();
const user = (request as any).user as User;
const rawIp = request.ip || request.socket.remoteAddress;
const ip = Array.isArray(rawIp) ? rawIp[0] : rawIp;
const user = (request as Request & { user?: User }).user;
const rawIp: string | string[] | undefined =
request.ip ?? request.socket.remoteAddress;
const ip: string | undefined = Array.isArray(rawIp) ? rawIp[0] : rawIp;
const userAgent = request.get('user-agent');
return next.handle().pipe(
tap(async (data) => {
try {
let entityId = null;
if (data && typeof data === 'object') {
if ('id' in data) entityId = String(data.id);
else if ('audit_id' in data) entityId = String(data.audit_id);
else if ('user_id' in data) entityId = String(data.user_id);
}
if (!entityId && request.params.id) {
entityId = String(request.params.id);
}
// ✅ FIX: ใช้ user?.user_id || null
const auditLog = this.auditLogRepo.create({
userId: user ? user.user_id : null,
action: auditMetadata.action,
entityType: auditMetadata.entityType,
entityId: entityId,
ipAddress: ip,
userAgent: userAgent,
severity: 'INFO',
} as unknown as AuditLog); // ✨ Trick: Cast ผ่าน unknown เพื่อล้าง Error ถ้า TS ยังไม่อัปเดต
await this.auditLogRepo.save(auditLog);
} catch (error) {
this.logger.error(
`Failed to create audit log for ${auditMetadata.action}: ${(error as Error).message}`,
);
}
}),
// Use void for fire-and-forget: tap() does not support async callbacks
tap((data: unknown) => {
void this.saveAuditLog(
data,
auditMetadata,
request,
user,
ip,
userAgent
);
})
);
}
/** Extracted async method to aoid "Promise returned in tap" lint warning */
private async saveAuditLog(
data: unknown,
auditMetadata: AuditMetadata,
request: Request,
user: User | undefined,
ip: string | undefined,
userAgent: string | undefined
): Promise<void> {
try {
let entityId: string | undefined;
if (data !== null && typeof data === 'object') {
const dataRecord = data as Record<string, unknown>;
if ('id' in dataRecord) {
entityId = String(dataRecord['id']);
} else if ('audit_id' in dataRecord) {
entityId = String(dataRecord['audit_id']);
} else if ('user_id' in dataRecord) {
entityId = String(dataRecord['user_id']);
}
}
if (!entityId && request.params['id']) {
entityId = String(request.params['id']);
}
const payload: CreateAuditLogPayload = {
userId: user?.user_id ?? null,
action: auditMetadata.action,
entityType: auditMetadata.entityType,
entityId,
ipAddress: ip,
userAgent,
severity: 'INFO',
};
const auditLog = this.auditLogRepo.create(payload as Partial<AuditLog>);
await this.auditLogRepo.save(auditLog);
} catch (error) {
this.logger.error(
`Failed to create audit log for ${auditMetadata.action}: ${(error as Error).message}`
);
}
}
}
@@ -1,3 +1,6 @@
// File: src/common/interceptors/transform.interceptor.ts
// Fix #1: แก้ไข `any` type ให้ถูกต้องตาม nestjs-best-practices (TypeScript Strict Mode)
import {
Injectable,
NestInterceptor,
@@ -7,40 +10,67 @@ import {
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
/** Metadata สำหรับ Paginated Response */
export interface ResponseMeta {
total: number;
page: number;
limit: number;
totalPages: number;
}
/** Standard API Response Wrapper */
export interface ApiResponse<T> {
statusCode: number;
message: string;
data: T;
meta?: any;
meta?: ResponseMeta;
}
/** Internal shape สำหรับ paginated data ที่ service ส่งมา */
interface PaginatedPayload<T> {
data: T[];
meta: ResponseMeta;
message?: string;
}
function isPaginatedPayload<T>(value: unknown): value is PaginatedPayload<T> {
return (
typeof value === 'object' &&
value !== null &&
'data' in value &&
'meta' in value &&
Array.isArray((value as PaginatedPayload<T>).data)
);
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
implements NestInterceptor<T, ApiResponse<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler
): Observable<Response<T>> {
next: CallHandler<T>
): Observable<ApiResponse<T>> {
return next.handle().pipe(
map((data: any) => {
const response = context.switchToHttp().getResponse();
map((data: T) => {
const response = context.switchToHttp().getResponse<{ statusCode: number }>();
// Handle Pagination Response (Standardize)
// ถ้า data มี structure { data: [], meta: {} } ให้ unzip ออกมา
if (data && data.data && data.meta) {
if (isPaginatedPayload(data)) {
return {
statusCode: response.statusCode,
message: data.message || 'Success',
data: data.data,
message: data.message ?? 'Success',
data: data.data as unknown as T,
meta: data.meta,
};
}
const dataAsRecord = data as Record<string, unknown>;
return {
statusCode: response.statusCode,
message: data?.message || 'Success',
data: data?.result || data,
message: (dataAsRecord?.['message'] as string | undefined) ?? 'Success',
data: (dataAsRecord?.['result'] as T | undefined) ?? data,
};
})
);