ApiResponse envelope
Implemented by ApiResponseInterceptor in src/common/interceptors/api-response.interceptor.ts. Type lives in src/common/types/api-response.ts.
Success shape
type ApiResponseSuccess<T> = {
requestId: string; // UUID
timestamp: string; // ISO 8601 UTC
status: "success";
statusCode: number; // 2xx
data: T;
error: null;
pagination?: {
cursor: string;
hasMore: boolean;
limit: number;
};
};
If the handler returns { data, pagination }, the interceptor unwraps and surfaces pagination at the envelope level.
Error shape
type ApiResponseError = {
requestId: string;
timestamp: string;
status: "error";
statusCode: number; // 4xx or 5xx
data: null;
error: {
code: string; // see ../05-api-reference/errors
message: string;
details?: Record<string, unknown>;
};
};
What controllers return
The controller method should return the payload, not the envelope. The interceptor wraps it.
@Get()
findAll(): Promise<AgentDto[]> {
return this.svc.findAll();
}
For paginated reads, return { data, pagination } and the interceptor pulls it apart:
@Get()
async list(@Query() q: ListQuery) {
const result = await this.svc.list(q);
return { data: result.rows, pagination: result.cursor };
}
What controllers throw
Throw HttpException (or any subclass: BadRequestException, ForbiddenException, etc.) with a structured payload:
throw new HttpException(
{ code: "QUOTA_EXCEEDED", message: "Out of sessions for this period", details: { remaining: 0, resetsAt: "..." } },
HttpStatus.TOO_MANY_REQUESTS,
);
ErrorHandlingInterceptor extracts code, message, details and assembles the envelope.
If you throw a non-HttpException, it becomes 500 INTERNAL_ERROR with error.message: 'Internal server error' and the original message logged.
When wrapping is skipped
| Endpoint | Why |
|---|---|
/api/auth/* | better-auth writes the response directly via toNodeHandler |
POST /webhooks/stripe | Returns a bare 200 or 4xx; Stripe expects no envelope |
GET /health | Liveness probe; clients (load balancers) don't parse JSON |
The interceptor detects res.headersSent and short-circuits.
Standardized fields
requestIdis the same UUID logged byLoggingInterceptorand emitted to PostHog. Cross-correlate any way you like.timestampis server clock at the moment the envelope is built.statusCodemirrors the HTTP status code on the wire (helpful when intermediaries strip it).