Skip to content
ExponaBLOG
NESTJS · DAY 031 / 100

Should you wrap every response in an envelope in NestJS

{ data, success, message } on every response — a debate worth having.

Expona Admin6 min read

After 30 days of building NestJS applications, you've probably noticed a pattern: should you wrap every response in an envelope? It's one of those architectural decisions that seems simple until you're three months into a project and realize you've made life harder for your frontend team, your API clients, and your own type generation tools.

The response envelope—that { data, success, message } wrapper around every endpoint—feels safe and consistent. But consistency without purpose is technical debt. Let me walk you through when envelopes help and when they actively hurt your codebase.

HTTP Status Already Says Success or Failure

Your first instinct might be: "I need a success: boolean field so clients always know what happened." But HTTP status codes exist for exactly this reason. When your endpoint returns 200 OK, the resource was successfully processed. A 400 Bad Request tells the client exactly what went wrong—without needing a wrapper field.

Here's what most envelope-heavy codebases look like:

typescript
// Endpoint returns 200 with success: false
{
  success: false,
  message: "Email already exists",
  data: null
}

// vs. proper HTTP semantics
// Status: 409 Conflict
{
  message: "Email already exists"
}

The first approach forces clients to check both the HTTP status and a boolean field. That's redundant. The second approach lets standard HTTP tooling (cURL, Postman, proxies, load balancers) understand your API correctly without parsing response bodies.

I learned this the hard way on a e-commerce platform. Our mobile team was checking the success field instead of status codes, and our analytics middleware was missing actual failures because the HTTP layer looked successful. When we switched to proper status codes, error handling became simpler across every client.

Use the correct HTTP status code. Always. That's your first signal of success or failure.

Envelopes Break OpenAPI Generators and Typing

This is where envelope wrapping creates real friction. If you wrap every response, your OpenAPI schema becomes noise:

typescript
// What you define
@Get('/users/:id')
async getUser(@Param('id') id: string): Promise<{ data: User; success: boolean; message: string }> {
  return { data: { id, name: 'John' }, success: true, message: 'User found' };
}

// What OpenAPI generates
{
  "responses": {
    "200": {
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "data": { "$ref": "#/components/schemas/User" },
              "success": { "type": "boolean" },
              "message": { "type": "string" }
            }
          }
        }
      }
    }
  }
}

Now a frontend developer reading your API docs sees { data: User } instead of just User. They have to unwrap it in every single endpoint. Your TypeScript types look verbose, and your response mapping code becomes repetitive.

Compare this to letting NestJS's @Serialize decorator and your DTO directly represent the response shape:

typescript
export class UserDto {
  id: string;
  name: string;
  email: string;
}

@Get('/users/:id')
async getUser(@Param('id') id: string): Promise<UserDto> {
  const user = await this.usersService.findById(id);
  return user;
}

Your OpenAPI schema is clean. Your types are clear. Your client code doesn't have boilerplate unwrapping logic.

On a SaaS platform I built, we had 40+ endpoints wrapped in envelopes. When we wanted to generate a TypeScript client from our OpenAPI spec, every single type had this wrapper. We spent a full sprint removing it—not because the envelope was wrong conceptually, but because it scaled poorly across a real product.

Use Envelopes Only for Pagination Metadata

There's one place where an envelope genuinely makes sense: paginated responses. You need to return the actual data, but also metadata like total count, current page, and whether there are more results.

typescript
export class PaginatedDto<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  hasMore: boolean;
}

@Get('/users')
async listUsers(
  @Query('page', ParseIntPipe) page: number = 1,
  @Query('limit', ParseIntPipe) limit: number = 10
): Promise<PaginatedDto<UserDto>> {
  const [users, total] = await this.usersService.findPaginated(page, limit);
  return {
    data: users,
    total,
    page,
    limit,
    hasMore: page * limit < total
  };
}

This envelope serves a purpose: it carries metadata that's contextual to the response. It's not redundant with HTTP status codes, and it scales well across your codebase because pagination is a consistent pattern.

On a ride-hailing platform, we used this pattern for driver listings, ride history, and support tickets. Every list endpoint returned a PaginatedDto. Frontend developers knew exactly what structure to expect, and our OpenAPI docs were still readable.

Consistency Matters More Than Either Choice

Here's the uncomfortable truth: the specific choice matters less than making it once and sticking with it. I've seen strong teams thrive with envelope-wrapped responses, and I've seen others excel with minimal wrapping.

What fails consistently is mixed approaches—some endpoints wrapped, others not. That creates cognitive load for every developer reading your API, and it makes client code harder to maintain.

If your team decides on envelope wrapping, enforce it with a consistent decorator or interceptor:

typescript
@Injectable()
export class ResponseWrapperInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(data => ({
        success: true,
        data,
        message: 'Request successful'
      }))
    );
  }
}

@UseInterceptors(ResponseWrapperInterceptor)
@Controller('/api')
export class AppController {
  // Every route is automatically wrapped
}

Or if you go minimal, document it clearly and ensure every response follows the same pattern without wrappers.

On a trading platform, we chose no envelopes except for paginated endpoints. We documented this decision in our API guidelines, and every code review checked for consistency. No surprises, no confusion.

Common Mistakes

  • Wrapping errors in envelopes: Error responses shouldn't return { success: false, data: null, message: "..." }. Use appropriate HTTP status codes and let your exception filter handle error structure consistently.
  • Redundant status fields: Never return { status: 'success', statusCode: 200, success: true } in the same response. Pick one mechanism and use it everywhere.
  • Inconsistent envelope depth: Don't wrap some endpoints and not others. Either commit to the pattern globally or don't use it at all.
  • Ignoring client tooling: If you're using OpenAPI generators, swagger-ui, or type-safe client libraries, test how envelopes affect them before standardizing on the pattern.
  • Over-nesting: Avoid { data: { data: { ... } } } by thinking through your envelope layers before they compound across nested resources.

Key Takeaways

  • HTTP status codes are your primary success/failure signal—don't duplicate that information in response bodies.
  • Envelope wrappers create friction with OpenAPI generation and type-safe client libraries; measure this cost before standardizing.
  • Pagination is a legitimate use case for envelope wrapping because it carries contextual metadata alongside data.
  • Consistency—whether wrapped or unwrapped—matters far more than the specific choice. Document it and enforce it across your codebase.
  • Use interceptors or decorators to enforce your chosen pattern globally rather than relying on developers to remember it endpoint-by-endpoint.

By day 31, you've built enough NestJS applications to recognize patterns. This decision will come up again. Make it thoughtfully, document it clearly, and keep your team aligned.

ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles