概要
APIからクライアントサイドにレスポンスする際に形式を統一したかったので実装してみた
{
"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
}
]
}
実装内容
解説
- データを詰め込むオブジェクトの型を定義して結合します
export const Info = {
statusCode: 200,
message: 'SUCCESS',
};
export type Response<T> = typeof Info & {
data: T;
};
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 テストはこんな感じになる
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);
});
});