Nest(TypeScript)で遊んでみる 〜Parameter Binding編〜
Posted on September 12, 2018 at 19:30 (JST)
今回はControllerにてRequestParameterから値を取得する方法について記載する。
- RequestParameterのbinding
- RouterParameters
- QueryString
- RequestBody
- 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にアクセスした時に id
はx1
、detailId
はy777
となる。
可変部分の値それぞれに対して変数を用意する以外で、下記のように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.id
はx1
、params.detailId
はy777
となる。
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
- Nest公式ドキュメント: Controller
- Nest公式ドキュメント: Custom route decorators
- Nest公式ドキュメント: Unit testing
- supertest (Github)