response
This commit is contained in:
@@ -0,0 +1,70 @@
|
|||||||
|
import { applyDecorators, Type } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiExtraModels,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiProperty,
|
||||||
|
getSchemaPath,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class ApiSuccessResponseDto<T = unknown> {
|
||||||
|
@ApiProperty({ example: true })
|
||||||
|
success: true;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, example: 'Operation completed successfully' })
|
||||||
|
message?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
data: T;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, type: Object, additionalProperties: true })
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiErrorResponseDto {
|
||||||
|
@ApiProperty({ example: false })
|
||||||
|
success: false;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 400 })
|
||||||
|
statusCode: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Validation failed' })
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
required: false,
|
||||||
|
type: [String],
|
||||||
|
example: ['mobile must be a valid phone number'],
|
||||||
|
})
|
||||||
|
errors?: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2026-07-02T12:00:00.000Z' })
|
||||||
|
timestamp: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '/api/users' })
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ApiStandardOkResponse = <TModel extends Type<unknown>>(
|
||||||
|
model: TModel,
|
||||||
|
options?: { isArray?: boolean },
|
||||||
|
) =>
|
||||||
|
applyDecorators(
|
||||||
|
ApiExtraModels(ApiSuccessResponseDto, model),
|
||||||
|
ApiOkResponse({
|
||||||
|
schema: {
|
||||||
|
allOf: [
|
||||||
|
{ $ref: getSchemaPath(ApiSuccessResponseDto) },
|
||||||
|
{
|
||||||
|
properties: {
|
||||||
|
data: options?.isArray
|
||||||
|
? {
|
||||||
|
type: 'array',
|
||||||
|
items: { $ref: getSchemaPath(model) },
|
||||||
|
}
|
||||||
|
: { $ref: getSchemaPath(model) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import {
|
||||||
|
ArgumentsHost,
|
||||||
|
Catch,
|
||||||
|
ExceptionFilter,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Logger,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { ApiErrorResponse } from '../interfaces/api-response.interface';
|
||||||
|
|
||||||
|
const SWAGGER_PATH_PREFIXES = ['/docs', '/docs-json', '/docs-yaml'];
|
||||||
|
|
||||||
|
@Catch()
|
||||||
|
export class ApiExceptionFilter implements ExceptionFilter {
|
||||||
|
private readonly logger = new Logger(ApiExceptionFilter.name);
|
||||||
|
|
||||||
|
catch(exception: unknown, host: ArgumentsHost): void {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse<Response>();
|
||||||
|
const request = ctx.getRequest<Request>();
|
||||||
|
|
||||||
|
if (SWAGGER_PATH_PREFIXES.some((prefix) => request.url.startsWith(prefix))) {
|
||||||
|
if (exception instanceof HttpException) {
|
||||||
|
response.status(exception.getStatus()).json(exception.getResponse());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
|
||||||
|
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
message: 'Internal server error',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorResponse = this.buildErrorResponse(exception, request.url);
|
||||||
|
|
||||||
|
if (errorResponse.statusCode >= HttpStatus.INTERNAL_SERVER_ERROR) {
|
||||||
|
this.logger.error(
|
||||||
|
`${request.method} ${request.url}`,
|
||||||
|
exception instanceof Error ? exception.stack : String(exception),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.status(errorResponse.statusCode).json(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildErrorResponse(
|
||||||
|
exception: unknown,
|
||||||
|
path: string,
|
||||||
|
): ApiErrorResponse {
|
||||||
|
let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
let message = 'Internal server error';
|
||||||
|
let errors: string[] | undefined;
|
||||||
|
|
||||||
|
if (exception instanceof HttpException) {
|
||||||
|
statusCode = exception.getStatus();
|
||||||
|
const exceptionResponse = exception.getResponse();
|
||||||
|
|
||||||
|
if (typeof exceptionResponse === 'string') {
|
||||||
|
message = exceptionResponse;
|
||||||
|
} else if (typeof exceptionResponse === 'object' && exceptionResponse) {
|
||||||
|
const responseBody = exceptionResponse as Record<string, unknown>;
|
||||||
|
const responseMessage = responseBody.message;
|
||||||
|
|
||||||
|
if (Array.isArray(responseMessage)) {
|
||||||
|
errors = responseMessage.filter(
|
||||||
|
(item): item is string => typeof item === 'string',
|
||||||
|
);
|
||||||
|
message =
|
||||||
|
errors.length > 0 ? 'Validation failed' : 'Bad Request';
|
||||||
|
} else if (typeof responseMessage === 'string') {
|
||||||
|
message = responseMessage;
|
||||||
|
} else if (typeof responseBody.error === 'string') {
|
||||||
|
message = responseBody.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
statusCode,
|
||||||
|
message,
|
||||||
|
...(errors?.length ? { errors } : {}),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
path,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
CallHandler,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import {
|
||||||
|
ApiSuccessResponse,
|
||||||
|
PaginatedResult,
|
||||||
|
} from '../interfaces/api-response.interface';
|
||||||
|
|
||||||
|
const SWAGGER_PATH_PREFIXES = ['/docs', '/docs-json', '/docs-yaml'];
|
||||||
|
|
||||||
|
function isApiResponse(value: unknown): value is ApiSuccessResponse {
|
||||||
|
return (
|
||||||
|
typeof value === 'object' &&
|
||||||
|
value !== null &&
|
||||||
|
'success' in value &&
|
||||||
|
(value as ApiSuccessResponse).success === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPaginatedResult<T>(value: unknown): value is PaginatedResult<T> {
|
||||||
|
return (
|
||||||
|
typeof value === 'object' &&
|
||||||
|
value !== null &&
|
||||||
|
'items' in value &&
|
||||||
|
'meta' in value &&
|
||||||
|
Array.isArray((value as PaginatedResult<T>).items)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMessageOnlyResponse(
|
||||||
|
value: unknown,
|
||||||
|
): value is { message: string } {
|
||||||
|
return (
|
||||||
|
typeof value === 'object' &&
|
||||||
|
value !== null &&
|
||||||
|
'message' in value &&
|
||||||
|
typeof (value as { message: unknown }).message === 'string' &&
|
||||||
|
Object.keys(value).length === 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TransformResponseInterceptor<T>
|
||||||
|
implements NestInterceptor<T, ApiSuccessResponse<T>>
|
||||||
|
{
|
||||||
|
intercept(
|
||||||
|
context: ExecutionContext,
|
||||||
|
next: CallHandler<T>,
|
||||||
|
): Observable<ApiSuccessResponse<T>> {
|
||||||
|
const request = context.switchToHttp().getRequest<Request>();
|
||||||
|
|
||||||
|
if (SWAGGER_PATH_PREFIXES.some((prefix) => request.url.startsWith(prefix))) {
|
||||||
|
return next.handle() as Observable<ApiSuccessResponse<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return next.handle().pipe(
|
||||||
|
map((data): ApiSuccessResponse<T> => {
|
||||||
|
if (isApiResponse(data)) {
|
||||||
|
return data as ApiSuccessResponse<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPaginatedResult<T>(data)) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: data.items as T,
|
||||||
|
meta: data.meta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMessageOnlyResponse(data)) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: data.message,
|
||||||
|
data: null as T,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: (data ?? null) as T,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
export interface ApiSuccessResponse<T = unknown> {
|
||||||
|
success: true;
|
||||||
|
message?: string;
|
||||||
|
data: T;
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiErrorResponse {
|
||||||
|
success: false;
|
||||||
|
statusCode: number;
|
||||||
|
message: string;
|
||||||
|
errors?: string[];
|
||||||
|
timestamp: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiResponse<T = unknown> = ApiSuccessResponse<T> | ApiErrorResponse;
|
||||||
|
|
||||||
|
export interface PaginatedResult<T> {
|
||||||
|
items: T[];
|
||||||
|
meta: Record<string, unknown>;
|
||||||
|
}
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import { ApiExceptionFilter } from './common/filters/api-exception.filter';
|
||||||
|
import { TransformResponseInterceptor } from './common/interceptors/transform-response.interceptor';
|
||||||
import { setupSwagger } from './swagger/swagger.setup';
|
import { setupSwagger } from './swagger/swagger.setup';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
app.useGlobalInterceptors(new TransformResponseInterceptor());
|
||||||
|
app.useGlobalFilters(new ApiExceptionFilter());
|
||||||
|
|
||||||
// Global validation pipe
|
// Global validation pipe
|
||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
|
|||||||
Reference in New Issue
Block a user