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'
}
}
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
// ...
}
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';
}
}
-
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;
-
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);
}
}
-
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;
}
}
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)
// ...
}
}
Thanks to @jmcdo29 for reviewing this article.
Top comments (9)
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"
sorry, I didn't follow what do you mean.
But yeah, there are lacks in the fw that breaks its claimed agnosticism.
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.
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
Real World example
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?
the docs site wasn't clear about it. I find it out by looking at the typescript definitions and by practicing
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'
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 useexpress
's response type