アレについて記す

Nest(TypeScript)で遊んでみる 〜Parameter Binding編〜

Posted on September 12, 2018 at 19:30 (JST)

今回はControllerにてRequestParameterから値を取得する方法について記載する。

  • RequestParameterのbinding
    1. RouterParameters
    2. QueryString
    3. RequestBody
    4. Headers
  • CustomDecoratorについて
  • e2e test

作成したコード [ nest-angular-sample: works/04_request_parameter_binding ]

動作環境

OS: macOS High Sierra ver. 10.13.4
Nodejs: v8.10.0
npm: 5.6.0
nest: 5.4.0

RequestParameterのbinding

エンドポイントとなるメソッドの引数にデコレータを記述することにより値を取得できる。
Nest公式ドキュメント: Controller

1. RouterParameters

URLの特定部分の値を取得したい場合は@Paramデコレータを使用する。

[src/samples/samples.controller.ts]

// パスから複数の値を取得
@Get(':id/details/:detailId')
routeParameters(
  @Param('id') id: string,
  @Param('detailId') detailId: string,
) {
  return {id, detailId};
}

上記の場合、GET http://example.com/x1/details/y777 のURLにアクセスした時に idx1detailIdy777となる。
可変部分の値それぞれに対して変数を用意する以外で、下記のようにDTOを指定して値を取得することもできる。

[src/samples/samples.controller.ts]

// パスの値からDTOを作成
@Get('params/:id/:detailId')
routeParametersToDto(
  @Param() params: RouteParameters,
) {
  return {params};
}

[src/samples/dto/samples.dto.ts]

export class RouteParameters {
  readonly id: string;
  readonly detailId: string;
}

この例では GET http://example.com/params/x1/y777 へアクセスした時に params.idx1params.detailIdy777となる。

2. QueryString

クエリストリングの値を取得したい場合は@Queryデコレータを使用する。

[src/samples/samples.controller.ts]

// クエリストリングから取得
@Get('queries')
queries(
  @Query('id') id: string,
  @Query('statuses') statuses: string[],
) {
  return {id, statuses};
}

この例では GET http://example.com/samples/queries?id=1&statuses[]=pendding&statuses[]=completed へアクセスした時に id は “1”、statuses は [“pendding”, “completed”] となる。

なお、クエリのstatusesに[]をつけなかった場合、変則的な挙動となるので注意が必要。
GET http://example.com/samples/queries?id=1&statuses=pendding&statuses=completedのstatuses は先ほどと同じ [“pendding”, “completed”](Array<string>)になるが、GET http://example.com/samples/queries?id=1&statuses=penddingの statuses は "pendding"(string)となる。


[src/samples/samples.controller.ts]

// クエリストリングからDTO作成
@Get('queries2dto')
queriesToDto(
  @Query() query: QueryParameters,
) {
  return {query};
}

[src/samples/dto/samples.dto.ts]

export class QueryParameters {
  readonly id: string;
  readonly statuses: string[];
}

値をDTOとして取得することも可能。

3. RequestBody

@Body デコレータを使用する。

[src/samples/samples.controller.ts]

// JSONからネストしたDTO作成
@Post('user')
@HttpCode(HttpStatus.ACCEPTED)
dtoNested(@Body() user: UserDto) {
  this.logger.debug('dtoNested: ' + JSON.stringify(user));
  return user;
}

[src/samples/dto/samples.dto.ts]

export class UserDto {
  readonly name: string;
  readonly contact: Contact;
}

export class Contact {
  readonly emails: string[];
  readonly phoneNumber: string;
}

ネストしたDTOにもすんなりmappingできる。
上記のコードの場合、下記のJSONをそのまま受け取れる。

{
  "name": "Bugs Bunny",
  "contact": {
    "emails": ["foo@example.com", "bar@example.com"],
    "phoneNumber": "0000-00-0000"
  }
}

4. Headers

RequestHeaderの値を取得する場合は@Headersデコレータを使用する。
似た名前の@Head(HttpMethodのHEADのエンドポイント指定に使用する)や@Header(ResponseのHeader項目を設定する)と間違えないよう要注意。

[src/samples/samples.controller.ts]

// Headerの値を取得
@Post('header')
@HttpCode(HttpStatus.ACCEPTED)
header(@Headers('x-auth-token') authToken: string) {
  this.logger.debug(`authToken: ${authToken}`);
  return { authToken };
}

@Headersデコレータに指定する項目名はすべて小文字にしないと値が取得できない点にも注意が必要。。。
client側のHeader項目指定は大文字/小文字どちらでもOK。

$ curl -X POST http://localhost:3000/samples/header -H "X-Auth-Token:hogehoge"
$ curl -X POST http://localhost:3000/samples/header -H "x-auth-token:hogehoge"

CustomDecoratorについて

自分でbind用デコレータを作成することができる。
Nestの公式ドキュメント

[src/samples/samples.controller.ts]

// CustomDecoratorで変換
@Post('userWithDecorator')
@HttpCode(HttpStatus.ACCEPTED)
withDecorator(@User() user: UserDto) {
  this.logger.debug('withDecorator: ' + JSON.stringify(user));
  return user;
}

[src/samples/decorators/user.decorator.ts]

import { createParamDecorator } from '@nestjs/common';

export const User = createParamDecorator((data, req) => {
  return req.body;
});

End-to-End test

e2eテストはsupertestを使用している。
テストの書き方はsupertestのReadmeが参考になる。

e2eテストは$ npm run test:e2eで実行できる。

[e2e/samples/samples.e2e-spec.ts]

import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { SamplesModule } from './../../src/samples/samples.module';
import { INestApplication } from '@nestjs/common';

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

  beforeAll(async () => {
    const moduleFixture = await Test.createTestingModule({
      imports: [SamplesModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('/samples (GET)', (done) => {
    return request(app.getHttpServer())
      .get('/samples')
      .expect(200, {message: 'Hello world!'}, done);
  });

  it('/samples/:id/details/:detailId (GET)', (done) => {
    return request(app.getHttpServer())
      .get('/samples/1/details/7')
      .expect(200, {
        detailId: '7',
        id: '1',
      }, done);
  });

  it('/samples/params/:id/:detailId (GET)', (done) => {
    return request(app.getHttpServer())
      .get('/samples/params/1/x77')
      .expect(200, {
        params: {
          id: '1',
          detailId: 'x77',
        },
      }, done);
  });

  it('/samples/queries (GET)', (done) => {
    return request(app.getHttpServer())
      .get('/samples/queries?id=1&statuses[]=pendding&statuses[]=completed')
      .expect(200, {
        id: '1',
        statuses: [ 'pendding', 'completed' ],
      }, done);
  });

  it('/samples/queries2dto (GET)', (done) => {
    return request(app.getHttpServer())
      .get('/samples/queries2dto?id=1&statuses[]=pendding&statuses[]=completed')
      .expect(200, {
        query: {
          id: '1',
          statuses: [ 'pendding', 'completed' ],
        },
      }, done);
  });

  it('/samples/user (POST)', (done) => {
    return request(app.getHttpServer())
      .post('/samples/user')
      .send(`{
        "name": "Bugs Bunny",
        "contact": {
          "emails": ["foo@example.com", "bar@example.com"],
          "phoneNumber": "0000-00-0000"
        }
      }`)
      .set('Content-Type', 'application/json')
      .expect(202, {
        name: 'Bugs Bunny',
        contact: {
          emails: ['foo@example.com', 'bar@example.com'],
          phoneNumber: '0000-00-0000',
        },
      }, done);
  });

  it('/samples/userWithDecorator (POST)', (done) => {
    return request(app.getHttpServer())
      .post('/samples/userWithDecorator')
      .send(`{
        "name": "Bugs Bunny",
        "contact": {
          "emails": ["foo@example.com", "bar@example.com"],
          "phoneNumber": "0000-00-0000"
        }
      }`)
      .set('Content-Type', 'application/json')
      .expect(202, {
        name: 'Bugs Bunny',
        contact: {
          emails: ['foo@example.com', 'bar@example.com'],
          phoneNumber: '0000-00-0000',
        },
      }, done);
  });

  it('/samples/header (POST)', (done) => {
    return request(app.getHttpServer())
      .post('/samples/header')
      .set('Content-Type', 'application/json')
      .set('X-Auth-Token', 'abcdefg')
      .expect(202, { authToken: 'abcdefg' }, done);
  });
});

POSTなど、RequestBodyをパースする必要がある場合はContent-Typeを正しく指定しないと想定通りに動かない点に注意。

下記のエラーが出る場合はimport設定が不正なので import * as request from 'supertest'; へ修正が必要。

TypeError: supertest_1.default is not a function

  18 |
  19 |   it('/samples (GET)', (done) => {
> 20 |     return request(app.getHttpServer())

以上。


参考URL