DEV Community

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

Posted on • Edited on

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

TL;DR

for NestJS v8, v9 and v10

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 able to switch to any other HTTP adapter easily 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 (9)

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.

Thread Thread
 
kasir-barati profile image
Mohammad Jawad (Kasir) Barati

Thanks for sharing the link. I am not sure how it works but I would love to ask you if you have or know a good repo on GitHub. Share it if you do so we can take a look at it

Thread Thread
 
kostyatretyak profile image
Костя Третяк
Collapse
 
kasir-barati profile image
Mohammad Jawad (Kasir) Barati

Many thanks, I was looking for something like this. BTW how did you figure it out that you can put all of these together? Doc?

Collapse
 
micalevisk profile image
Micael Levi L. C.

the docs site wasn't clear about it. I find it out by looking at the typescript definitions and by practicing

Collapse
 
nishantjawla profile image
Nishant Jawla

Sorry the question might be a beginner level I am new to NestJS and typescript

How did you known where to import the typing from ?
import type { Response } from 'express'

Collapse
 
micalevisk profile image
Micael Levi L. C.

it's mentioned in the nestjs docs. If you're using the default http adapter, then nestjs will use express and so this is why we need to use express's response type