diff --git a/apps/backend/src/common/dto/api-response.dto.ts b/apps/backend/src/common/dto/api-response.dto.ts new file mode 100644 index 0000000..ff612e5 --- /dev/null +++ b/apps/backend/src/common/dto/api-response.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ApiResponseMeta { + @ApiProperty({ nullable: true }) + cachedAt: string | null; + + @ApiProperty() + fromCache: boolean; + + constructor(fromCache: boolean, cachedAt: string | null = null) { + this.fromCache = fromCache; + this.cachedAt = cachedAt; + } +} + +export class ApiResponse { + data: T; + meta: ApiResponseMeta; + + constructor(data: T, fromCache = false, cachedAt: string | null = null) { + this.data = data; + this.meta = new ApiResponseMeta(fromCache, cachedAt); + } +} diff --git a/apps/backend/src/common/dto/pagination.dto.ts b/apps/backend/src/common/dto/pagination.dto.ts new file mode 100644 index 0000000..cb707f5 --- /dev/null +++ b/apps/backend/src/common/dto/pagination.dto.ts @@ -0,0 +1,20 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsOptional, IsInt, Min, Max } from 'class-validator'; + +export class PaginationDto { + @ApiPropertyOptional({ default: 1 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = 1; + + @ApiPropertyOptional({ default: 20 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit?: number = 20; +} diff --git a/apps/backend/src/common/filters/http-exception.filter.ts b/apps/backend/src/common/filters/http-exception.filter.ts new file mode 100644 index 0000000..373205f --- /dev/null +++ b/apps/backend/src/common/filters/http-exception.filter.ts @@ -0,0 +1,44 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { Response } from 'express'; + +@Catch() +export class HttpExceptionFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + let status = HttpStatus.INTERNAL_SERVER_ERROR; + let message = 'Internal server error'; + let error = 'Internal Server Error'; + + if (exception instanceof HttpException) { + status = exception.getStatus(); + const res = exception.getResponse(); + if (typeof res === 'string') { + message = res; + error = exception.name; + } else if (typeof res === 'object') { + const r = res as Record; + message = (r.message as string) || message; + error = (r.error as string) || exception.name; + } + } else if (exception instanceof Error) { + message = exception.message; + } + + response.status(status).json({ + statusCode: status, + message, + error, + timestamp: new Date().toISOString(), + path: request.url, + }); + } +} diff --git a/apps/backend/src/common/interceptors/transform.interceptor.ts b/apps/backend/src/common/interceptors/transform.interceptor.ts new file mode 100644 index 0000000..b91ca26 --- /dev/null +++ b/apps/backend/src/common/interceptors/transform.interceptor.ts @@ -0,0 +1,26 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { ApiResponse } from '../dto/api-response.dto'; + +@Injectable() +export class TransformInterceptor + implements NestInterceptor> +{ + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable> { + return next.handle().pipe( + map((data) => { + if (data instanceof ApiResponse) return data; + return new ApiResponse(data, false, null); + }), + ); + } +} diff --git a/apps/backend/src/common/middleware/request-logging.middleware.ts b/apps/backend/src/common/middleware/request-logging.middleware.ts new file mode 100644 index 0000000..989077e --- /dev/null +++ b/apps/backend/src/common/middleware/request-logging.middleware.ts @@ -0,0 +1,20 @@ +import { Injectable, NestMiddleware, Logger } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; + +@Injectable() +export class RequestLoggingMiddleware implements NestMiddleware { + private logger = new Logger('HTTP'); + + use(req: Request, res: Response, next: NextFunction): void { + const { method, originalUrl } = req; + const start = Date.now(); + + res.on('finish', () => { + const { statusCode } = res; + const duration = Date.now() - start; + this.logger.log(`${method} ${originalUrl} ${statusCode} ${duration}ms`); + }); + + next(); + } +}