DEV Community ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป

Micael Levi L. C.
Micael Levi L. C.

Posted on • Updated on

NestJS tip: platform-agnostic way to define response status/headers

TL;DR

for NestJS v8 and v9

If you want to change the default status code dynamically, or want to define HTTP headers dynamically, you don't have to switch to the library-specific mode!

You could have something like this:

import { Controller, Get } from '@nestjs/common'
import { GenericResponse } from './generic-response.decorator'
//       ^~ this is new
@Controller()
export class AppController {
  @Get()
  getHello(@GenericResponse() res: GenericResponse): string {
    res
      .setHeader('X-Header', 'foo') // <<
      .setStatus(201) // <<

    return 'Hello World'
  }
}
Enter fullscreen mode Exit fullscreen mode

What?

Sometimes we need to change the status code of the response in controller's method with some dynamic value. To do so, you could do the following:

import { Res } from '@nestjs/common'
import type { Response } from 'express'
// ...
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  const statusCode = something ? 200 : 201
  res.status(statusCode) // <- Using library-specific response object's method
  // ...
}
Enter fullscreen mode Exit fullscreen mode

As you can see above, since we're using the @Res() (or @Response()) decorator) we've switched to the library-specific mode. Thus, the res parameter will be an object created by the underlying HTTP server (ExpressJS, in this example).

By taking that approach, our NestJS app starts breaking its well known platform agnosticism. So we won't be switch to any other HTTP adapter that doesn't follows the same API for response object as Express without changing others parts of the source code other than main.ts ๐Ÿ˜ž

The same applies for HTTP headers (@Header()) too. The only way to define a header dynamically at the controller level is by using the lib-specific response object -- in this case it would be res.set('X-Header', 'Foo')

Solution

To me, a better solution for such problem will be having another abstraction layer on top of the response object (what I've called AbstractResponse). We can leverage on execution context object to retrieve the current response object, while using the HttpAdapterHost to access high-level operations that are implemented by the HTTP adapter. Finally, we can use Pipes along with createParamDecorator utility to build the brigde between all of that while making the interface for this new behavior looks better.

Now we are no longer manipulating the library-specific response object ๐Ÿฅณ which will make our code more reusable across different HTTP adapters (or even other kinds of adapters, if you play with that a bit)

Here's the full code in a playground: https://stackblitz.com/edit/nestjs-abs-res


  • app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { GenericResponse } from './generic-response.decorator';

@Controller()
export class AppController {
  @Get()
  getHello(@GenericResponse() res: GenericResponse): string {
    res
      .setHeader('X-Header', 'foo')
      .setStatus(201);

    return 'Hello';
  }
}
Enter fullscreen mode Exit fullscreen mode
  • generic-response.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { AbstractResponse } from './abstract-response';
import { ExecutionContextToAbstractResponsePipe } from './ctx-to-abstract-response.pipe';

const GetExecutionContext = createParamDecorator(
  (
    _: never,
    ctx: ExecutionContext,
  ): ExecutionContext => ctx,
);

export const GenericResponse = (): ParameterDecorator =>
  GetExecutionContext(ExecutionContextToAbstractResponsePipe);

export type GenericResponse = AbstractResponse;
Enter fullscreen mode Exit fullscreen mode
  • ctx-to-abstract-response.pipe.ts
import { Injectable, PipeTransform, ExecutionContext } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { AbstractResponse } from './abstract-response'

@Injectable()
export class ExecutionContextToAbstractResponsePipe implements PipeTransform {
  constructor(
    private readonly httpAdapterHost: HttpAdapterHost,
  ) {}

  transform(ctx: ExecutionContext): AbstractResponse {
    return new AbstractResponse(this.httpAdapterHost.httpAdapter, ctx);
  }
}
Enter fullscreen mode Exit fullscreen mode
  • abstract-response.ts
import type { ExecutionContext } from '@nestjs/common';
import type { HttpArgumentsHost } from '@nestjs/common/interfaces';
import { AbstractHttpAdapter } from '@nestjs/core';

export class AbstractResponse { // Note that this isn't a provider
  httpCtx: HttpArgumentsHost;

  constructor(
    private readonly httpAdapter: AbstractHttpAdapter,
    readonly executionContext: ExecutionContext,
  ) {
    this.httpCtx = executionContext.switchToHttp();
  }

  /** Define the HTTP header on the supplied response object. */
  setHeader(name: string, value: string): this {
    this.httpAdapter.setHeader(this.httpCtx.getResponse(), name, value);
    return this;
  }

  /** Define the HTTP status code on the supplied response object. */
  setStatus(statusCode: number): this {
    this.httpAdapter.status(this.httpCtx.getResponse(), statusCode);
    return this;
  }
}
Enter fullscreen mode Exit fullscreen mode

A minimal solution

Do note that all of this could be also accomplished by just injecting the HttpAdapterHost into the controller and calling the desired methods of the httpAdapterHost.httpAdapter object like so:

import { Controller, Get, Res } from '@nestjs/common'
import { HttpAdapterHost } from '@nestjs/core'
import type { Response } from 'express'

@Controller()
export class AppController {
  constructor(private readonly httpAdapterHost: HttpAdapterHost<ExpressAdapter>) {}

  @Get()
  findAll(@Res({ passthrough: true }) res: Response) {
    const { httpAdapter } = this.httpAdapterHost
    httpAdapter.setHeader(res, 'X-Header', 'Foo')
    httpAdapter.status(res, 201)
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Thanks to @jmcdo29 for reviewing this article.

Top comments (3)

Collapse
 
kostyatretyak profile image
ะšะพัั‚ั ะขั€ะตั‚ัะบ

You know that there are similar, but significantly simpler and more consistent frameworks. That's why you need to use it, not NestJS, which is not really "platform-agnostic"

Collapse
 
micalevisk profile image
Micael Levi L. C.

sorry, I didn't follow what do you mean.

But yeah, there are lacks in the fw that breaks its claimed agnosticism.

Collapse
 
kostyatretyak profile image
ะšะพัั‚ั ะขั€ะตั‚ัะบ

I mean Ditsmod. This framework doesn't have Express or Fastify under the hood, so it doesn't need to do the crutches you described in this post.

๐ŸŒš Friends don't let friends browse without dark mode.

Sorry, it's true.