From 264bf1bf266685fdcab17e3c3a096b2f694e91e5 Mon Sep 17 00:00:00 2001 From: Maxim Date: Wed, 20 May 2026 17:01:31 +0300 Subject: [PATCH 1/2] feat(app): add SkipResponseValidation decorator to bypass response validation --- src/shared/decorators/index.ts | 1 + .../decorators/skip-response-validation.decorator.ts | 4 ++++ .../interceptors/zod-validation.interceptor.ts | 12 ++++++++++++ 3 files changed, 17 insertions(+) create mode 100644 src/shared/decorators/skip-response-validation.decorator.ts diff --git a/src/shared/decorators/index.ts b/src/shared/decorators/index.ts index baf933f..132aa07 100644 --- a/src/shared/decorators/index.ts +++ b/src/shared/decorators/index.ts @@ -1,3 +1,4 @@ export { ApiBaseController } from './api-controller.decorator'; export { IS_PUBLIC_KEY, Public } from './public.decorator'; export * from './user.decorator'; +export { SkipResponseValidation } from './skip-response-validation.decorator'; diff --git a/src/shared/decorators/skip-response-validation.decorator.ts b/src/shared/decorators/skip-response-validation.decorator.ts new file mode 100644 index 0000000..c8c2225 --- /dev/null +++ b/src/shared/decorators/skip-response-validation.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const SKIP_RESPONSE_VALIDATION_KEY = 'SKIP_RESPONSE_VALIDATION_KEY'; +export const SkipResponseValidation = () => SetMetadata(SKIP_RESPONSE_VALIDATION_KEY, true); diff --git a/src/shared/interceptors/zod-validation.interceptor.ts b/src/shared/interceptors/zod-validation.interceptor.ts index 54b4cd5..6ab1781 100644 --- a/src/shared/interceptors/zod-validation.interceptor.ts +++ b/src/shared/interceptors/zod-validation.interceptor.ts @@ -9,6 +9,7 @@ import { Reflector } from '@nestjs/core'; import { map, Observable } from 'rxjs'; import { BaseException } from '@shared/error'; import { z } from 'zod/v4'; +import { SKIP_RESPONSE_VALIDATION_KEY } from '@shared/decorators/skip-response-validation.decorator'; export const ZOD_RESPONSE_TOKEN = 'ZOD_RESPONSE_TOKEN'; @@ -18,6 +19,17 @@ export class ZodValidationInterceptor implements NestInterceptor): Observable { const handler = context.getHandler(); + const controller = context.getClass(); + + const isSkipped = this.reflector.getAllAndOverride(SKIP_RESPONSE_VALIDATION_KEY, [ + handler, + controller, + ]); + + if (isSkipped) { + return next.handle(); + } + const metadata = this.reflector.get<{ schema: z.ZodTypeAny } | undefined>( ZOD_RESPONSE_TOKEN, handler, From 6419bb1e9748203dc68eaa1114cbb052d03ef869 Mon Sep 17 00:00:00 2001 From: Maxim Date: Wed, 20 May 2026 17:04:06 +0300 Subject: [PATCH 2/2] feat(metrics): add MetricsModule with endpoint for Prometheus metrics --- libs/metrics/metrics.controller.ts | 13 +++++++++++++ libs/metrics/metrics.module.ts | 17 +++++++++++++++++ package.json | 1 + pnpm-lock.yaml | 3 +++ src/app.module.ts | 11 ++--------- 5 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 libs/metrics/metrics.controller.ts create mode 100644 libs/metrics/metrics.module.ts diff --git a/libs/metrics/metrics.controller.ts b/libs/metrics/metrics.controller.ts new file mode 100644 index 0000000..b3267fd --- /dev/null +++ b/libs/metrics/metrics.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get, Header } from '@nestjs/common'; +import * as client from 'prom-client'; +import { SkipResponseValidation } from '@shared/decorators'; + +@Controller() +export class MetricsController { + @Get('dump') + @Header('Content-Type', client.register.contentType) + @SkipResponseValidation() + async getMetrics() { + return client.register.metrics(); + } +} diff --git a/libs/metrics/metrics.module.ts b/libs/metrics/metrics.module.ts new file mode 100644 index 0000000..b1a04ef --- /dev/null +++ b/libs/metrics/metrics.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { PrometheusModule } from '@willsoto/nestjs-prometheus'; +import { MetricsController } from './metrics.controller'; + +@Module({ + imports: [ + PrometheusModule.registerAsync({ + useFactory: () => ({ + defaultMetrics: { + enabled: process.env.NODE_ENV !== 'test', + }, + }), + }), + ], + controllers: [MetricsController], +}) +export class MetricsModule {} diff --git a/package.json b/package.json index 3d4302f..c0f4bd2 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "passport": "^0.7.0", "passport-jwt": "^4.0.1", "postgres": "^3.4.9", + "prom-client": "^15.1.3", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "transliteration": "^2.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef2954e..44f45a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,6 +131,9 @@ importers: postgres: specifier: ^3.4.9 version: 3.4.9 + prom-client: + specifier: ^15.1.3 + version: 15.1.3 reflect-metadata: specifier: ^0.2.0 version: 0.2.2 diff --git a/src/app.module.ts b/src/app.module.ts index 538199e..c646c24 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,7 +5,6 @@ import { ConfigService } from '@nestjs/config'; import * as schema from './shared/entities'; import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; import { ZodValidationPipe } from 'nestjs-zod'; -import { PrometheusModule } from '@willsoto/nestjs-prometheus'; import { HealthModule } from '@libs/health'; import { UserModule } from './user'; import { GlobalExceptionFilter } from '@shared/error'; @@ -24,18 +23,12 @@ import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; import { ICacheService } from '@shared/adapters/cache/ports'; import { DatabaseHealthService } from '@libs/database'; import { ZodValidationInterceptor } from '@shared/interceptors/zod-validation.interceptor'; +import { MetricsModule } from '../libs/metrics/metrics.module'; @Module({ imports: [ ConfigModule, - PrometheusModule.registerAsync({ - useFactory: () => ({ - path: 'dump', - defaultMetrics: { - enabled: process.env.NODE_ENV !== 'test', - }, - }), - }), + MetricsModule, DatabaseModule.registerAsync({ global: true, inject: [ConfigService],