The NestJS API Response Format You Won't Regret
Your API has 30 endpoints. Some return { data: [...] }, some return arrays directly, some return { result: {...}, status: 'ok' }. The error format? Depends on who wrote the endpoint.
Your frontend team is writing if (response.data) ... else if (response.result) ... everywhere. It's week 3 and they're already frustrated.
The Problem
NestJS doesn't enforce a response format. Controllers return whatever shape you give them:
// Endpoint A returns this
{ id: 1, name: 'Alice' }
// Endpoint B returns this
{ data: { id: 1, name: 'Alice' }, message: 'success' }
// Error from endpoint A
{ statusCode: 404, message: 'Not Found' }
// Error from endpoint B
{ error: true, msg: 'user not found', code: 'USER_NOT_FOUND' }Every frontend dev has to learn each endpoint's quirks. Every new backend dev invents yet another format.
The Solution: Wrap Once, Everywhere
A NestJS interceptor can transform every response into a consistent envelope:
// Every success response becomes:
{
"success": true,
"statusCode": 200,
"data": { "id": 1, "name": "Alice" }
}
// Every error response becomes:
{
"success": false,
"statusCode": 404,
"error": {
"code": "USER_NOT_FOUND",
"message": "User not found"
}
}The frontend only needs one type:
type ApiResponse<T> = {
success: boolean;
statusCode: number;
data?: T;
error?: { code: string; message: string };
};Why Do This on Day One?
Changing response formats after launch is painful:
- Every frontend consumer needs to update their parsing logic
- Mobile apps with older versions can't handle the new format
- API clients built by third parties break silently
Adding the wrapper before any frontend integration means you never have to migrate.
The Performance Question
"Doesn't wrapping every response add overhead?"
We measured it:
| Scenario | Raw NestJS | With wrapper | Difference |
|---|---|---|---|
| Success (200) | 0.61ms | 0.44ms | -0.17ms |
| Error (404) | 0.39ms | 0.52ms | +0.13ms |
The measured differences were sub-millisecond in this local run. The success result should not be interpreted as the wrapper making NestJS faster; benchmark variance can be larger than the measured delta. Re-run the benchmark on your deployment path before setting a latency budget.
What Else Comes Free?
Once you have a global interceptor, you can add metadata without changing controllers:
- Request ID tracking — optional
X-Request-Idhandling and response metadata - Response time — optional
meta.responseTime - Pagination metadata —
{ meta: { pagination: { page, totalPages, ... } } } - Rate limit info — optional mirroring of
X-RateLimit-*headers intometa.rateLimit - Deprecation warnings — RFC 9745 headers + response metadata
Each of these would be a separate interceptor if you built them yourself.
Using @nestarc/safe-response
@nestarc/safe-response does all of this:
npm install @nestarc/safe-response// app.module.ts — enable only the metadata your API contract needs
SafeResponseModule.register({
requestId: true,
responseTime: true,
rateLimit: true,
}),No controller or service changes are needed for the base envelope. Request IDs, response time, and rate-limit metadata are opt-in; their defaults are disabled.
Includes Swagger schema auto-generation, error code mapping, cursor/offset pagination metadata, i18n adapter, and zero-dependency TypeScript client types.
Documentation · GitHub · Benchmark
Primary framework reference: NestJS interceptors.