Skip to content

Commit cfa2bb5

Browse files
author
Ahmad Abdulkareem
authored
Merge pull request #153 from akargi/feat/issue151
2 parents 759dca7 + 35b6935 commit cfa2bb5

File tree

8 files changed

+144
-13
lines changed

8 files changed

+144
-13
lines changed

apps/api/src/main.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
1111
import { GlobalValidationPipe } from '@app/common/pipes/global-validation.pipe';
1212
import { GlobalExceptionFilter } from '@app/common/filters/global-exception.filter';
1313
import { LoggingInterceptor } from '@app/common/interceptors/logging.interceptor';
14+
import { ErrorResponse } from '@app/common/dto/error-response.dto';
1415
import helmet from 'helmet'; // Import Helmet
1516
import * as cors from 'cors'; // Import CORS
1617
import * as rateLimit from 'express-rate-limit'; // Import rateLimit
@@ -63,9 +64,17 @@ async function bootstrap() {
6364
.addBearerAuth()
6465
.build();
6566

66-
const document = SwaggerModule.createDocument(app, config);
67+
const document = SwaggerModule.createDocument(app, config, {
68+
extraModels: [ErrorResponse],
69+
});
6770
SwaggerModule.setup('api/docs', app, document);
6871

72+
// Ensure unhandled promise rejections are logged and do not crash silently
73+
process.on('unhandledRejection', (reason) => {
74+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
75+
console.error('Unhandled Rejection at:', reason);
76+
});
77+
6978
await app.listen(process.env.PORT || DEFAULT_PORT);
7079
}
7180
void bootstrap();
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
3+
export class ErrorResponse {
4+
@ApiProperty({ example: 400 })
5+
statusCode: number;
6+
7+
@ApiProperty({ example: 'BadRequest' })
8+
error: string;
9+
10+
@ApiProperty({ example: 'A descriptive error message' })
11+
message: string | string[];
12+
13+
@ApiProperty({ example: 'ERR_INVALID_TRADE', required: false })
14+
code?: string;
15+
16+
@ApiProperty({ example: {}, required: false })
17+
details?: unknown;
18+
19+
@ApiProperty({ example: '/api/trades' })
20+
path: string;
21+
22+
@ApiProperty({ example: new Date().toISOString() })
23+
timestamp: string;
24+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { HttpException, HttpStatus } from '@nestjs/common';
2+
3+
export class AppError extends HttpException {
4+
public readonly code: string;
5+
public readonly details?: unknown;
6+
7+
constructor(
8+
code: string,
9+
message: string | string[],
10+
status: HttpStatus = HttpStatus.BAD_REQUEST,
11+
details?: unknown,
12+
) {
13+
super({ error: 'AppError', message, code, details }, status);
14+
this.code = code;
15+
this.details = details;
16+
}
17+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export enum ErrorCodes {
2+
GENERIC = 'ERR_GENERIC',
3+
INSUFFICIENT_BALANCE = 'ERR_INSUFFICIENT_BALANCE',
4+
INVALID_TRADE = 'ERR_INVALID_TRADE',
5+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { HttpStatus } from '@nestjs/common';
2+
import { AppError } from './app-error';
3+
import { ErrorCodes } from './error-codes.enum';
4+
5+
export class InsufficientBalanceException extends AppError {
6+
constructor(details?: unknown) {
7+
super(
8+
ErrorCodes.INSUFFICIENT_BALANCE,
9+
'Insufficient balance to complete this operation',
10+
HttpStatus.PAYMENT_REQUIRED,
11+
details,
12+
);
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { HttpStatus } from '@nestjs/common';
2+
import { AppError } from './app-error';
3+
import { ErrorCodes } from './error-codes.enum';
4+
5+
export class InvalidTradeException extends AppError {
6+
constructor(details?: unknown) {
7+
super(
8+
ErrorCodes.INVALID_TRADE,
9+
'The trade request is invalid or malformed',
10+
HttpStatus.BAD_REQUEST,
11+
details,
12+
);
13+
}
14+
}

libs/common/src/filters/global-exception.filter.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
HttpStatus,
77
} from '@nestjs/common';
88
import { Request, Response } from 'express';
9+
import { AppError } from '../exceptions/app-error';
10+
import { ErrorCodes } from '../exceptions/error-codes.enum';
911

1012
@Catch()
1113
export class GlobalExceptionFilter implements ExceptionFilter {
@@ -15,31 +17,72 @@ export class GlobalExceptionFilter implements ExceptionFilter {
1517
const request = ctx.getRequest<Request>();
1618

1719
let status = HttpStatus.INTERNAL_SERVER_ERROR;
18-
let errorResponse: any = {
20+
let payload: Record<string, unknown> = {
1921
statusCode: status,
2022
error: 'InternalServerError',
2123
message: 'An unexpected error occurred',
24+
code: ErrorCodes.GENERIC,
2225
path: request.url,
2326
timestamp: new Date().toISOString(),
2427
};
2528

26-
if (exception instanceof HttpException) {
29+
if (exception instanceof AppError) {
30+
// AppError already structures message/code/details
31+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
32+
const res: any = exception.getResponse();
2733
status = exception.getStatus();
28-
const res = exception.getResponse();
29-
30-
errorResponse = {
34+
payload = {
35+
statusCode: status,
36+
error: res.error ?? exception.name,
37+
message: res.message ?? exception.message,
38+
code: res.code ?? (exception as AppError).code,
39+
details: res.details ?? (exception as AppError).details,
40+
path: request.url,
41+
timestamp: new Date().toISOString(),
42+
};
43+
} else if (exception instanceof HttpException) {
44+
status = exception.getStatus();
45+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
46+
const res: any = exception.getResponse();
47+
payload = {
3148
statusCode: status,
32-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
33-
error: res['error'] ?? exception.name,
34-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
35-
message: res['message'] ?? exception.message,
36-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
37-
details: res['details'],
49+
error: res.error ?? exception.name,
50+
message: res.message ?? exception.message,
51+
code: res.code ?? ErrorCodes.GENERIC,
52+
details: res.details,
3853
path: request.url,
3954
timestamp: new Date().toISOString(),
4055
};
4156
}
4257

43-
response.status(status).json(errorResponse);
58+
// Structured error logging for observability / tracking
59+
try {
60+
const logPayload = {
61+
level: 'error',
62+
message:
63+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
64+
(payload['message'] as unknown) ?? 'Unhandled exception',
65+
error: exception instanceof Error ? exception.stack : exception,
66+
request: {
67+
method: request.method,
68+
url: request.url,
69+
params: request.params,
70+
query: request.query,
71+
},
72+
code: payload['code'],
73+
timestamp: new Date().toISOString(),
74+
};
75+
76+
// Replace with an external tracker (Sentry, Datadog) integration here
77+
// For now, print structured JSON to console so logs can be parsed.
78+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
79+
console.error(JSON.stringify(logPayload));
80+
} catch (logErr) {
81+
// If logging fails, still respond to client
82+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
83+
console.error('Failed to log error', logErr);
84+
}
85+
86+
response.status(status).json(payload);
4487
}
4588
}

libs/common/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@ export * from './constants';
33
export * from './interfaces';
44
export * from './mailer';
55
export * from './enums';
6+
export * from './dto/error-response.dto';
7+
export * from './exceptions/app-error';
8+
export * from './exceptions/error-codes.enum';
9+
export * from './exceptions/insufficient-balance.exception';
10+
export * from './exceptions/invalid-trade.exception';

0 commit comments

Comments
 (0)