MVP реализации MoexVibe — NestJS бэкенд + React фронтенд + CI/CD #1

Merged
ksv741 merged 21 commits from feat/mvp-implementation into main 2026-06-13 21:07:34 +03:00
5 changed files with 134 additions and 0 deletions
Showing only changes of commit 5dcc1e12be - Show all commits

View File

@ -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<T> {
data: T;
meta: ApiResponseMeta;
constructor(data: T, fromCache = false, cachedAt: string | null = null) {
this.data = data;
this.meta = new ApiResponseMeta(fromCache, cachedAt);
}
}

View File

@ -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;
}

View File

@ -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<Response>();
const request = ctx.getRequest<Request>();
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<string, unknown>;
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,
});
}
}

View File

@ -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<T>
implements NestInterceptor<T, ApiResponse<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<ApiResponse<T>> {
return next.handle().pipe(
map((data) => {
if (data instanceof ApiResponse) return data;
return new ApiResponse(data, false, null);
}),
);
}
}

View File

@ -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();
}
}