Skip to content
By nestarc
Compatibility

@nestarc/safe-response 0.15.x and NestJS 10/11

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:

typescript
// 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:

typescript
// 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:

typescript
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:

ScenarioRaw NestJSWith wrapperDifference
Success (200)0.61ms0.44ms-0.17ms
Error (404)0.39ms0.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-Id handling and response metadata
  • Response time — optional meta.responseTime
  • Pagination metadata{ meta: { pagination: { page, totalPages, ... } } }
  • Rate limit info — optional mirroring of X-RateLimit-* headers into meta.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:

bash
npm install @nestarc/safe-response
typescript
// 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.

Last updated:

Released under the MIT License.