From df18ea1519843637c9522ebc5c54a4230d56882d Mon Sep 17 00:00:00 2001 From: soorq Date: Thu, 23 Apr 2026 21:47:53 +0300 Subject: [PATCH] fix: correct api versioning and improve global exception filter --- libs/bootstrap/src/bootstrap.ts | 26 ++++++++++---- .../src/interfaces/options.interface.ts | 1 + libs/bootstrap/src/setups/swagger.ts | 12 +++++-- src/main.ts | 2 +- .../auth/strategies/cookie.strategy.ts | 1 - src/shared/error/filter.ts | 35 ++++++++++++++++++- 6 files changed, 64 insertions(+), 13 deletions(-) diff --git a/libs/bootstrap/src/bootstrap.ts b/libs/bootstrap/src/bootstrap.ts index 39fb6bc..93e09cc 100644 --- a/libs/bootstrap/src/bootstrap.ts +++ b/libs/bootstrap/src/bootstrap.ts @@ -1,4 +1,4 @@ -import { Logger } from '@nestjs/common'; +import { Logger, VersioningType } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; import { setupThrottler } from './setups/throttler'; @@ -23,7 +23,8 @@ export async function bootstrapApp(options: BootstrapOptions) { const { appModule, - apiPrefix = 'api/v1', + apiPrefix, + version = 'v1', serviceName = 'App', portEnvKey = 'PORT', defaultPort = 3000, @@ -62,6 +63,15 @@ export async function bootstrapApp(options: BootstrapOptions) { }); if (apiPrefix) app.setGlobalPrefix(apiPrefix); + if (version) { + const hasV = version.startsWith('v'); + + app.enableVersioning({ + type: VersioningType.URI, + prefix: hasV ? 'v' : '', + defaultVersion: hasV ? version.slice(1) : version, + }); + } if (useCors) setupCors(app, origins); if (swaggerOptions) { const { path = 'docs', ...metadata } = swaggerOptions; @@ -96,7 +106,11 @@ export async function bootstrapApp(options: BootstrapOptions) { if (setupApp) setupApp(app); await app.listen(port, '0.0.0.0', (_err, address) => { - const baseUrl = `${address}${apiPrefix ? '/' + apiPrefix : ''}`; + const prefix = [apiPrefix, version].filter(Boolean).join('/'); + const baseUrl = `${address}${prefix ? '/' + prefix : ''}`; + + const swaggerBase = `${address}${apiPrefix ? '/' + apiPrefix : ''}`; + const swaggerPath = swaggerOptions?.path ?? 'docs'; if (_err) { logger.error(_err); @@ -107,10 +121,8 @@ export async function bootstrapApp(options: BootstrapOptions) { logger.verbose(`Environment: ${process.env.NODE_ENV || 'development'}`); logger.verbose(`API Endpoint: ${baseUrl}`); logger.verbose(`Health Check: ${baseUrl}/health`); - logger.verbose(`Swagger UI: ${baseUrl}/${swaggerOptions?.path ?? 'docs'}`); - logger.verbose( - `OpenAPI (Specs): ${baseUrl}/${swaggerOptions?.path ?? 'docs'}/s/{json,yaml}`, - ); + logger.verbose(`Swagger UI: ${swaggerBase}/${swaggerPath}`); + logger.verbose(`OpenAPI (Specs): ${swaggerBase}/${swaggerPath}/s/{json,yaml}`); logger.verbose(`Boot Time: ${startupTime}ms`); }); } diff --git a/libs/bootstrap/src/interfaces/options.interface.ts b/libs/bootstrap/src/interfaces/options.interface.ts index fed3ded..3d42a76 100644 --- a/libs/bootstrap/src/interfaces/options.interface.ts +++ b/libs/bootstrap/src/interfaces/options.interface.ts @@ -23,6 +23,7 @@ export interface SwaggerOptions extends SwaggerMetadata, SwaggerInfrastructure { export interface BootstrapOptions { apiPrefix?: string; + version?: string; appModule: Type; defaultPort?: number; portEnvKey?: keyof Config; diff --git a/libs/bootstrap/src/setups/swagger.ts b/libs/bootstrap/src/setups/swagger.ts index 9a838b6..b18afe4 100644 --- a/libs/bootstrap/src/setups/swagger.ts +++ b/libs/bootstrap/src/setups/swagger.ts @@ -28,9 +28,15 @@ export async function setupSwagger(app: NestFastifyApplication, options: Swagger .setVersion(version) .addBearerAuth(); - if (port) builder.addServer(`http://localhost:${port}`, 'Local'); - if (stage) builder.addServer(`https://api.${stage}`, 'Staging'); - if (domain) builder.addServer(`https://api.${domain}`, 'Production'); + if ((!stage || !domain) && port) { + builder.addServer(`http://localhost:${port}`, 'Local'); + } + if (stage) { + builder.addServer(`https://api.${stage}`, 'Staging'); + } + if (domain) { + builder.addServer(`https://api.${domain}`, 'Production'); + } const document = SwaggerModule.createDocument(app, builder.build(), { extraModels: [GlobalErrorResponse.Output], diff --git a/src/main.ts b/src/main.ts index c58168d..5f7ae4e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ import { AppModule } from './modules/app/app.module'; bootstrapApp({ serviceName: 'Tracker Monolit', appModule: AppModule, - apiPrefix: 'api/v1', + version: 'v1', defaultPort: 2000, portEnvKey: 'PORT', swaggerOptions: { diff --git a/src/modules/auth/strategies/cookie.strategy.ts b/src/modules/auth/strategies/cookie.strategy.ts index 7255ee7..d9334eb 100644 --- a/src/modules/auth/strategies/cookie.strategy.ts +++ b/src/modules/auth/strategies/cookie.strategy.ts @@ -22,7 +22,6 @@ export class CookieStrategy extends PassportStrategy(Strategy, 'cookie') { } validate(_req: FastifyRequest, payload: JwtPayload) { - console.log(_req, payload); if (!payload || !payload.jti) { throw new BaseException( { diff --git a/src/shared/error/filter.ts b/src/shared/error/filter.ts index f698ce8..2a8b778 100644 --- a/src/shared/error/filter.ts +++ b/src/shared/error/filter.ts @@ -1,4 +1,10 @@ -import { type ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from '@nestjs/common'; +import { + type ArgumentsHost, + Catch, + ExceptionFilter, + HttpException, + HttpStatus, +} from '@nestjs/common'; import { ZodValidationException } from 'nestjs-zod'; import type { FastifyReply, FastifyRequest } from 'fastify'; import { DatabaseError } from 'pg'; @@ -20,6 +26,10 @@ export class GlobalExceptionFilter implements ExceptionFilter { return this.parseHttp(exception, host); } + if (exception instanceof HttpException) { + return this.parseNestHttp(exception, host); + } + if (exception instanceof DrizzleQueryError) { return this.parseDatabase(exception, host); } @@ -93,6 +103,29 @@ export class GlobalExceptionFilter implements ExceptionFilter { ); }; + private parseNestHttp = async (exception: HttpException, host: ArgumentsHost) => { + const { request, response } = this.getCtxBase(host); + const status = exception.getStatus(); + const res = exception.getResponse(); + + const message = + typeof res === 'object' && res['message'] ? res['message'] : exception.message; + + const code = + typeof res === 'object' && res['error'] + ? res['error'].toUpperCase().replace(/\s+/g, '_') + : 'HTTP_EXCEPTION'; + + return response.status(status).send( + this.formatErrorResponse(request, status, { + code, + message, + stack: exception.stack, + details: [], + }), + ); + }; + private handleUnknownError(exception: any, host: ArgumentsHost) { const { request, response } = this.getCtxBase(host); const status = HttpStatus.INTERNAL_SERVER_ERROR;