Response API yang Konsisten — Kenapa Penting dan Bagaimana Menerapkannya di NestJS
Setiap endpoint mengembalikan amplop yang sama, baik sukses maupun gagal — bentuk response yang layak disepakati, serta global interceptor dan exception filter yang menegakkannya di NestJS.

Sebagai frontend engineer, salah satu hal yang paling bikin pusing selama bertahun-tahun adalah menghadapi response API yang tidak konsisten.
Kadang response sukses bentuknya begini:
{
"data": [...],
"meta": {...}
}Di lain waktu bentuknya begini:
{
"result": [...],
"pagination": {...}
}Lalu untuk error? Malah lebih parah — kadang { "error": "Something went wrong" }, kadang { "message": "Invalid request" }.
Ketidakkonsistenan seperti ini bukan sekadar menjengkelkan — ia memperlambat pengembangan, memperbanyak bug, dan membuat penanganan error jadi lebih sulit.
Tulisan ini membahas kenapa konsistensi response API itu penting, dan bagaimana Anda bisa menegakkannya di NestJS lewat sebuah global interceptor dan exception filter.
Kenapa Konsistensi Itu Penting
Baik Anda membangun API internal untuk tim sendiri maupun API publik untuk ribuan developer, struktur response yang konsisten berarti:
- Parsing yang bisa ditebak – frontend tidak perlu menebak-nebak strukturnya.
- Penanganan error jadi sederhana – satu handler untuk semua endpoint.
- Dokumentasi lebih mudah – skemanya cukup didefinisikan sekali lalu dipakai ulang di mana-mana.
- DX (Developer Experience) lebih baik – beban pikirannya berkurang, iterasinya lebih cepat.
Struktur Response API yang Saya Pakai
Berikut format yang biasa saya gunakan:
{
"method": "GET",
"path": "/users",
"timestamp": "2025-08-15T10:00:00.000Z",
"statusCode": 200,
"success": true,
"data": [],
"meta": null,
"error": null
}Dan untuk error:
{
"method": "GET",
"path": "/users",
"timestamp": "2025-08-15T10:00:00.000Z",
"statusCode": 400,
"success": false,
"data": null,
"meta": null,
"error": {
"type": "Bad Request",
"message": "Validation failed",
"errors": {
"email": ["Email is required"]
}
}
}Alur Response API
Berikut siklus dasar sebuah request API dengan format yang konsisten:
- Request Diterima — API menerima HTTP request dari client.
- Logika Bisnis Dijalankan — controller/service menangani proses utamanya.
- Response Interceptor — membungkus semua response sukses dalam format yang sama.
- Exception Filter — menangkap semua error dan memformatnya secara konsisten.
- Response Akhir Dikirim — frontend selalu menerima JSON yang bisa ditebak.
Implementasinya di NestJS
Kita bisa menegakkan konsistensi ini secara global lewat dua hal:
- Sebuah ResponseInterceptor untuk response sukses
- Sebuah HttpExceptionFilter untuk error
ResponseInterceptor Global
import {
CallHandler,
ExecutionContext,
HttpException,
Injectable,
NestInterceptor,
} from "@nestjs/common";
import { catchError, map, Observable, throwError } from "rxjs";
import { Request, Response } from "express";
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();
return next.handle().pipe(
map((data) => this.handleSuccess(request, response, data)),
catchError((err) => {
const statusCode = err instanceof HttpException ? err.getStatus() : 500;
return throwError(() => new HttpException(err, statusCode));
}),
);
}
private handleSuccess(request: Request, response: Response, data: any) {
const responseData = {
method: request.method,
path: request.url,
timestamp: new Date().toISOString(),
statusCode: response.statusCode,
success: true,
data: data,
meta: null,
error: null,
};
if (data?.data && data?.meta) {
responseData.data = data.data;
responseData.meta = data.meta;
}
return responseData;
}
}HttpExceptionFilter Global
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from "@nestjs/common";
import { Request, Response } from "express";
import { STATUS_CODES } from "http";
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse() as any;
const error = {
type:
STATUS_CODES?.[status] || exceptionResponse.response?.error || exception.name || "Error",
message:
status === 500
? "Internal Server Error"
: exceptionResponse?.response?.message || exception.message,
errors: exceptionResponse?.response?.validation || null,
};
const responseData = {
method: request.method,
path: request.url,
timestamp: new Date().toISOString(),
statusCode: status,
success: false,
data: null,
meta: null,
error,
};
response.status(status).json(responseData);
}
}Menerapkannya secara global di main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ResponseInterceptor } from "./interceptors/response.interceptor";
import { HttpExceptionFilter } from "./filters/http-exception.filter";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new ResponseInterceptor());
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();Hasilnya
Dengan setup ini:
- Setiap response sukses akan punya struktur yang sama.
- Setiap error akan mengikuti format yang sama.
- Para frontend engineer akan menyayangi Anda (atau setidaknya berhenti mengeluh 😆).
Bonus: Anda bisa dengan mudah menambahkan meta untuk paginasi atau errors untuk validasi tanpa mengubah bentuk keseluruhannya.
Inti pentingnya: konsistensi lebih penting daripada struktur persisnya. Begitu Anda menetapkan bentuknya, terapkan di semua endpoint.


