Skip to main content

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

EndpointWhy
/api/auth/*better-auth writes the response directly via toNodeHandler
POST /webhooks/stripeReturns a bare 200 or 4xx; Stripe expects no envelope
GET /healthLiveness probe; clients (load balancers) don't parse JSON

The interceptor detects res.headersSent and short-circuits.

Standardized fields

  • requestId is the same UUID logged by LoggingInterceptor and emitted to PostHog. Cross-correlate any way you like.
  • timestamp is server clock at the moment the envelope is built.
  • statusCode mirrors the HTTP status code on the wire (helpful when intermediaries strip it).