【NestJS】戻り値のデータ形式を変換して統一する方法

# 概要

APIからクライアントサイドにレスポンスする際に形式を統一したかったので実装してみた

https://docs.nestjs.com/interceptors

```bash
{
  "statusCode": 200,
  "message": "SUCCESS",
  "data": [
    {
      "name": "hoge",
      "email": "hoge@example.com",
      "password": "$2b$10$XuKMQ36pzZ/PSed3nT5djeuf.RzCrAPFL5bquZFnXgwO8CWLNxt0i",
      "id": "1",
      "ins_ts": "2022-04-09T16:17:06.732Z",
      "upd_ts": "2022-04-09T17:09:31.000Z",
      "delete_ts": null
    }
  ]
}
```

# 実装内容

## 解説

- データを詰め込むオブジェクトの型を定義して結合します

https://maku.blog/p/b4i46ah/

https://qiita.com/k-penguin-sato/items/9baa959e8919157afcd4

```ts
export const Info = {
  statusCode: 200,
  message: 'SUCCESS',
};

export type Response<T> = typeof Info & {
  data: T;
};
```



https://zenn.dev/lacolaco/books/angular-after-tutorial/viewer/http-interceptors

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/assign



```ts:src/transform.interceptor.ts
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';

export const Info = {
  statusCode: 200,
  message: 'SUCCESS',
};

export type Response<T> = typeof Info & {
  data: T;
};

@Injectable()
export class TransformInterceptor<T>
  implements NestInterceptor<T, Response<T>>
{
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<Response<T>> {
    return next.handle().pipe(map((data) => Object.assign({}, Info, { data })));
  }
}
```

# Unit テストはこんな感じになる

https://github.com/nestjs/nest/blob/master/integration/hello-world/e2e/interceptors.spec.ts

```ts:src/transform.interceptor.spec.ts
// https://github.com/nestjs/nest/blob/master/integration/hello-world/e2e/interceptors.spec.ts
import {
  CallHandler,
  ExecutionContext,
  INestApplication,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { map, of } from 'rxjs';
import { Test } from '@nestjs/testing';
import { APP_INTERCEPTOR } from '@nestjs/core';
import * as request from 'supertest';
import { AppModule } from './app.module';

const RETURN_VALUE = 'test';

@Injectable()
export class OverrideInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    /**
     * Observableが最初にサブスクライブされた時に呼ばれる関数
     * 値をパブリッシュされた対象を確認・通知を受け取ります
     */
    return of(RETURN_VALUE);
  }
}

@Injectable()
export class TransformInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    // RxJS いつ起こるかわからない処理を待ち受けて応答オブジェクトをプロパティに詰めて返す
    return next.handle().pipe(map((data) => ({ data })));
  }
}

@Injectable()
export class StatusInterceptor {
  constructor(private readonly statusCode: number) {}

  intercept(context: ExecutionContext, next: CallHandler) {
    // HTTP アプリケーションのコンテキストに適した HttpArgumentsHost オブジェクトを返却
    const ctx = context.switchToHttp();
    const res = ctx.getResponse();
    res.status(this.statusCode);
    return next.handle().pipe(map((data) => ({ data })));
  }
}

@Injectable()
export class HeaderInterceptor {
  constructor(private readonly headers: object) {}

  intercept(context: ExecutionContext, next: CallHandler) {
    const ctx = context.switchToHttp();
    const res = ctx.getResponse();
    // 必要なヘッダーをセット
    for (const key in this.headers) {
      if (this.headers.hasOwnProperty(key)) {
        res.header(key, this.headers[key]);
      }
    }
    return next.handle().pipe(map((data) => ({ data })));
  }
}

function createTestModule(interceptor) {
  return Test.createTestingModule({
    imports: [AppModule],
    providers: [
      {
        provide: APP_INTERCEPTOR,
        useValue: interceptor,
      },
    ],
  }).compile();
}

describe('Interceptors', () => {
  let app: INestApplication;

  it(`should transform response (sync)`, async () => {
    app = (
      await createTestModule(new OverrideInterceptor())
    ).createNestApplication();

    await app.init();
    return request(app.getHttpServer()).get('/users').expect(200, RETURN_VALUE);
  });
});

```