NestJSでResponeオブジェクトをDIして使う。


この記事はNestJS AdventCalendar 2023の24日の記事です。

# 結論

NestJSでResponseオブジェクトをDIして使う場合は、2重でレスポンスしないようにしましょう。


例
```typescript
import { ExpressAdapter } from "@nestjs/platform-express";

export class CustomAdapter extends ExpressAdapter {
    public reply(response: any, body: any, statusCode?: number) {
        if (body !== undefined) {
            return super.reply(response, body, statusCode);
        }
    }
}
```

# 何がしたいの？

ControllerとPresenterを分けたいと考えました。

## NestJSのControllerの挙動

NestJSでは、Controllerのreturn値がそのままHttpResponseになります。

```typescript
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello() {
    return {hello: 'world'};
  }
}
```

例えば、上記のように実装したControllerでは以下のようなレスポンスが得られます。

```json
{
  "hello": "world"
}
```

これは呼び出されるメソッドがvoidの場合でも動作します。
その時のレスポンスbodyは空でレスポンスを返そうとします。

以下のようなメソッドでも空のレスポンスを返すエントリーポイントが作成できます。

```typescript
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello() {}
}
```

## 表示ロジックを分離する

表示に関わるロジックが複雑な場合、表示用のロジックのみ別の場所に切り出してテストしやすい構造にしたいと考えるかもしれません。
その場合、手取り早くAppModuleでResponseオブジェクトを登録してPresenterを作ることができそうです。

```typescript
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AppPresenter } from './app.presenter';
import { Request } from 'express';
import { REQUEST } from '@nestjs/core';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    AppPresenter,
    {
      provide: 'RESPONSE',
      useFactory: (request: Request) => request.res,
      inject: [REQUEST],
    },
  ],
})
export class AppModule {}
```

```typescript
import { Inject, Injectable, Scope } from '@nestjs/common';
import { Response } from 'express';

@Injectable({scope: Scope.REQUEST})
export class AppPresenter {
  constructor(@Inject('RESPONSE') private readonly response: Response) {}

  output(value: number) {
    this.response.send({value});
  }
}

```

ただ、この時に何かの非同期処理をしていたりすると、おそらく以下のようなエラーが出ます。

```
Error: Cannot set headers after they are sent to the client
```

なぜならば、Adapterからもresponseを返しているにも関わらず、Presenterからもresponseを返してしまうためです。

これは、上述の説明の通りControllerで呼び出されたメソッドがvoidとして空のレスポンスを返し、その後PresenterでもResponseを返しているため多重呼び出しとなります。

これを解消するためには、[Controllerのレスポンスを受け取ってレスポンスを生成しているHttpServerのreplyメソッド](https://github.com/nestjs/nest/blob/18335ffa9d53d7efd4f1529761f3539f035aaa79/packages/core/router/router-response-controller.ts#L38)でresponseを返さないようにすれば良さそうです。

## なぜResponse Decoratorは動くのか

NestJSには[Response Decorator](https://docs.nestjs.com/controllers#request-object)があります。

それを使うとresponseオブジェクトを取り出して以下のように書くことができます。

```typescript
import { Controller, Get, Response } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(@Response() res) {
    setTimeout(() => {
      res.send({hello: 'world'});
    }, 1000);
  }
}
```

この時もgetHelloはvoidです。
しかし、同様のエラーは出力されません。
これはResponseデコレータを利用するとpassthroughというフラグが立ち、voidの時はresponseをsendしないようになるためです。


:::note warn
この挙動については公式ドキュメントでも以下のように記載されています。

Note that when you inject either @Res() or @Response() in a method handler, you put Nest into Library-specific mode for that handler, and you become responsible for managing the response.
:::

つまり、Responseデコレータを呼び出すとControllerのメソッドからの返り値を無視してレスポンスを自由にDIしても良さそうです。

# 結論

NestJSでResponseオブジェクトをDIして使う場合は、2重でレスポンスしないようにしましょう。

例えば、愚直にHTTP Adapterを継承してbodyがundefinedである場合はレスポンスを返さないようにするのも良いかもしれないです。

```typescript
import { ExpressAdapter } from "@nestjs/platform-express";

export class CustomAdapter extends ExpressAdapter {
    public reply(response: any, body: any, statusCode?: number) {
        if (body !== undefined) {
            return super.reply(response, body, statusCode);
        }
    }
}
```

明示的にResponseを別で逃がしていることがわかるようにResponse decoratorを握り潰すのも良いかもしれないです。

```typescript
import { Controller, Get, Response } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(@Response() _) {
    this.appService.exec();
  }
}
```

~~わざわざDIせずとも、Controllerで複雑なロジックを処理するために切り出した関数やクラスを呼び出せばこんなトリッキーなことしなくても良いのでそのようなアプローチを考えた方が筋が良さそう。~~