Skip to content

Commit 8dcb9b8

Browse files
response: add ResponseEnvelopeInterceptor and standardize error envelope in HttpExceptionFilter
1 parent 97cabf9 commit 8dcb9b8

File tree

3 files changed

+54
-4
lines changed

3 files changed

+54
-4
lines changed

src/common/filters/exception.filter.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,16 @@ export class HttpExceptionFilter implements ExceptionFilter {
2525
// no-op: never block response on logging failures
2626
}
2727

28+
const payload = exception.getResponse?.();
2829
response.status(status).json({
29-
statusCode: status,
30-
timestamp: new Date().toISOString(),
31-
path: request.path,
32-
exception: exception.getResponse(),
30+
success: false,
31+
error: {
32+
statusCode: status,
33+
message: (payload as any)?.message || exception.message,
34+
details: payload,
35+
path: request.path,
36+
timestamp: new Date().toISOString(),
37+
},
3338
});
3439
}
3540
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
2+
import { Observable } from 'rxjs';
3+
import { map } from 'rxjs/operators';
4+
5+
/**
6+
* ResponseEnvelopeInterceptor
7+
*
8+
* Wraps successful JSON responses in a standard envelope when the handler or its
9+
* method metadata indicates it. Envelope shape:
10+
* { success: boolean; data?: any; error?: { code: string; message: string; details?: any } }
11+
*
12+
* Automatically detects if data already matches { success, data } to avoid double wrapping.
13+
*/
14+
@Injectable()
15+
export class ResponseEnvelopeInterceptor implements NestInterceptor {
16+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
17+
const ctx = context.switchToHttp();
18+
const request = ctx.getRequest();
19+
const response = ctx.getResponse();
20+
21+
const handler = context.getHandler();
22+
const wantsEnvelope = Reflect.getMetadata('cb:envelope', handler);
23+
24+
if (!wantsEnvelope) {
25+
return next.handle();
26+
}
27+
28+
return next.handle().pipe(
29+
map((data) => {
30+
// Avoid wrapping if already in desired form
31+
if (data && typeof data === 'object' && 'success' in data && ('data' in data || 'error' in data)) {
32+
return data;
33+
}
34+
35+
return {
36+
success: true,
37+
data,
38+
};
39+
})
40+
);
41+
}
42+
}

src/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
4949
import { PrismaClientExceptionFilter } from 'nestjs-prisma';
5050
import { NestExpressApplication } from '@nestjs/platform-express';
5151
import { HttpExceptionFilter } from './common/filters/exception.filter'; // Assuming path to your filter
52+
import { ResponseEnvelopeInterceptor } from './common/interceptors/response-envelope.interceptor';
5253

5354
import helmet from 'helmet';
5455
import cookieParser from 'cookie-parser';
@@ -133,6 +134,8 @@ async function bootstrap(): Promise<void> {
133134
// Global filters for exception handling.
134135
const { httpAdapter } = app.get(HttpAdapterHost);
135136
app.useGlobalFilters(new HttpExceptionFilter(), new PrismaClientExceptionFilter(httpAdapter));
137+
// Global interceptor for standardized response envelopes (only wraps endpoints marked with envelope flag)
138+
app.useGlobalInterceptors(new ResponseEnvelopeInterceptor());
136139

137140
// ===================================
138141
// WebSocket Adapter (Optional)

0 commit comments

Comments
 (0)