0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NestJSAdvent Calendar 2023

Day 24

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

Last updated at Posted at 2023-12-23

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

結論

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

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になります。

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では以下のようなレスポンスが得られます。

{
  "hello": "world"
}

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

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

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を作ることができそうです。

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 {}
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メソッドでresponseを返さないようにすれば良さそうです。

なぜResponse Decoratorは動くのか

NestJSにはResponse Decoratorがあります。

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

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 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である場合はレスポンスを返さないようにするのも良いかもしれないです。

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を握り潰すのも良いかもしれないです。

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で複雑なロジックを処理するために切り出した関数やクラスを呼び出せばこんなトリッキーなことしなくても良いのでそのようなアプローチを考えた方が筋が良さそう。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?