Nest.jsでWebサービスを呼び出す方法

Nest.jsでWebサービスを呼び出す方法のメモです。このサンプルプログラムは[GitHub](https://github.com/YoheiFukuhara/node-call-rest)に置いています。

# 環境

|種類|バージョン|備考|
|:-:|:-:|:-:|
|OS|Ubuntu18.04.01 LTS|仮想で動かしています|
|nvm|0.35.3|もっと良さそうなのもあるかもしれないですが、特にこだわりなく使っています|
|Node.js|12.16.2|2020/4時点最新のLTSを使っています|
|npm|6.14.4|2020/4時点最新|
|nest cli|7.1.2|2020/4時点最新|
|NEST|7.0.0|2020/4時点最新|

```bash
$ nest info

 _   _             _      ___  _____  _____  _     _____
| \ | |           | |    |_  |/  ___|/  __ \| |   |_   _|
|  \| |  ___  ___ | |_     | |\ `--. | /  \/| |     | |
| . ` | / _ \/ __|| __|    | | `--. \| |    | |     | |
| |\  ||  __/\__ \| |_ /\__/ //\__/ /| \__/\| |_____| |_
\_| \_/ \___||___/ \__|\____/ \____/  \____/\_____/\___/


[System Information]
OS Version     : Linux 4.15
NodeJS Version : v12.16.2
NPM Version    : 6.14.4 

[Nest CLI]
Nest CLI Version : 7.1.2 

[Nest Platform Information]
platform-express version : 7.0.0
common version           : 7.0.0
core version             : 7.0.0
```

# 手順
## 1. 開発
### 1.1. プロジェクト作成
プロジェクト作成予定のディレクトリから`nest new <プロジェクト名>`でプロジェクト作成。途中プロンプトでプロジェクト名"node-call-rest"を入力し、パッケージマネージャーに"npm"を選択。
※ `git clone`でリポジトリをcloneした後に実行するとgit情報が上書きされてしまいました。 `--skip-git`オプションを使ってもだめでした(まるで調べていないので、やり方が悪いだけかも)。

```bash
# From Node root directory
$ nest new node-call-rest
⚡  We will scaffold your app in a few seconds..

? What name would you like to use for the new project? node-call-rest
CREATE node-call-rest/.eslintrc.js (599 bytes)
CREATE node-call-rest/.prettierrc (51 bytes)
CREATE node-call-rest/README.md (3370 bytes)
CREATE node-call-rest/nest-cli.json (64 bytes)
CREATE node-call-rest/package.json (1902 bytes)
CREATE node-call-rest/tsconfig.build.json (97 bytes)
CREATE node-call-rest/tsconfig.json (336 bytes)
CREATE node-call-rest/src/app.controller.spec.ts (617 bytes)
CREATE node-call-rest/src/app.controller.ts (274 bytes)
CREATE node-call-rest/src/app.module.ts (249 bytes)
CREATE node-call-rest/src/app.service.ts (142 bytes)
CREATE node-call-rest/src/main.ts (208 bytes)
CREATE node-call-rest/test/app.e2e-spec.ts (630 bytes)
CREATE node-call-rest/test/jest-e2e.json (183 bytes)

? Which package manager would you ❤️  to use? npm
✔ Installation in progress... ☕

🚀  Successfully created project node-call-rest
👉  Get started with the following commands:

$ cd node-call-rest
$ npm run start
              Thanks for installing Nest 🙏
     Please consider donating to our open collective
            to help us maintain this package.
                
                
   🍷  Donate: https://opencollective.com/nest
```

### 1.2. プロジェクト作成の動作確認
うまく作れているか実行して確認します。デフォルトでルートに"Hello World!"を返すようになっているので、それを使って確認します。

```bash
# From Project root directory
$ npm run starg:debug
[1:27:21 PM] File change detected. Starting incremental compilation...

[1:27:21 PM] Found 0 errors. Watching for file changes.

Debugger listening on ws://127.0.0.1:9229/c6466736-e752-434a-a7be-a105e6731a7f
For help, see: https://nodejs.org/en/docs/inspector
[Nest] 11160   - 04/20/2020, 1:27:22 PM   [NestFactory] Starting Nest application...
[Nest] 11160   - 04/20/2020, 1:27:22 PM   [InstanceLoader] AppModule dependencies initialized +12ms
[Nest] 11160   - 04/20/2020, 1:27:22 PM   [RoutesResolver] AppController {}: +5ms
[Nest] 11160   - 04/20/2020, 1:27:22 PM   [RouterExplorer] Mapped {, GET} route +3ms
[Nest] 11160   - 04/20/2020, 1:27:22 PM   [NestApplication] Nest application successfully started +2ms
```

Curlで確認。Hello Worldが返ってきます。

```bash
$ curl localhost:3000
Hello World!
```

### 1.3. `app.module.ts`変更
`src/app.module.ts`を変更。`HttpModule`をインポートします(1行目と7行目を変更)。

```typescript:app.module.ts
import { Module, HttpModule} from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { RestController } from './rest/rest.controller';

@Module({
  imports: [HttpModule],
  controllers: [AppController, RestController],
  providers: [AppService],
})
export class AppModule {}
```

### 1.4. コントローラー作成
NEST cliでコントローラー`rest`を作成。

```bash
$ nest g controller rest
CREATE src/rest/rest.controller.spec.ts (479 bytes)
CREATE src/rest/rest.controller.ts (97 bytes)
UPDATE src/app.module.ts (322 bytes)
```

作成された`src/rest/rest.controller.ts`を変更。
今回はGetリクエストを受けて、"[httpbin.org](httpbin.org)"というサービスに対してPOSTリクエストを投げます。このサービスにBASIC認証は不要ですが、実際にはよく使うと思われるため付け加えておきました。

```typescript:src/rest/rest.controller.ts
import { Controller, Get, HttpService } from '@nestjs/common';

@Controller('rest')
export class RestController {

    constructor(private httpService: HttpService) { }

    @Get()
    async callRest() {

        const url = "http://httpbin.org/post";

        //authは今回は不要だが、入れるとしたらこんな形式
        const auth = {
            auth: {
                username: 'user name',
                password: 'password'
            }
        };
        const body = { "contents": "test" };
        // APIコール
        try {
            var result = await this.httpService.post(url, body, auth).toPromise();
        }
        catch (e) {
            console.log(e.response);
        }
        console.log(result);
        return result.data;
    }
}
```

## 2. 動作確認
### 2.1. Node.js起動
スクリプトを使ってNode.jsを起動します。

```bash
# From Project root directory
$ npm run starg:debug
```

### 2.2. Getリクエストを投げる
curlを使ってlocalhost:3000/restにGetリクエストを投げます。"[httpbin.org](httpbin.org)"へのpostリクエストの結果が返ってきます。見にくいので改行・インデントをして結果を整形しました。`json`以下にPostリクエストBodyがjsonで入っています。

```bash
$ curl localhost:3000/rest
{
    "args": {},
    "data": "{\"contents\":\"test\"}",
    "files": {},
    "form": {},
    "headers": {
        "Accept": "application/json, text/plain, */*",
        "Authorization": "Basic dXNlciBuYW1lOnBhc3N3b3Jk",
        "Content-Length": "19",
        "Content-Type": "application/json;charset=utf-8",
        "Host": "httpbin.org",
        "User-Agent": "axios/0.19.2",
        "X-Amzn-Trace-Id": "Root=1-5e9d303c-3f9f4b3c5621034ae617c097"
    },
    "json": {
        "contents": "test"
    },
    "origin": "<my ip address>",
    "url": "http://httpbin.org/post"
}
```

`npm run starg:debug`を実行したターミナルでは、ログにもっと細かい情報を出しています。

```bash
[2:16:40 PM] File change detected. Starting incremental compilation...

[2:16:41 PM] Found 0 errors. Watching for file changes.

Debugger listening on ws://127.0.0.1:9229/2c52a7c8-dc62-485c-90c9-94b91200de8e
For help, see: https://nodejs.org/en/docs/inspector
[Nest] 12063   - 04/20/2020, 2:16:41 PM   [NestFactory] Starting Nest application...
[Nest] 12063   - 04/20/2020, 2:16:41 PM   [InstanceLoader] HttpModule dependencies initialized +16ms
[Nest] 12063   - 04/20/2020, 2:16:41 PM   [InstanceLoader] AppModule dependencies initialized +2ms
[Nest] 12063   - 04/20/2020, 2:16:41 PM   [RoutesResolver] AppController {}: +5ms
[Nest] 12063   - 04/20/2020, 2:16:41 PM   [RouterExplorer] Mapped {, GET} route +3ms
[Nest] 12063   - 04/20/2020, 2:16:41 PM   [RoutesResolver] RestController {/rest}: +1ms
[Nest] 12063   - 04/20/2020, 2:16:41 PM   [RouterExplorer] Mapped {/rest, GET} route +1ms
[Nest] 12063   - 04/20/2020, 2:16:41 PM   [NestApplication] Nest application successfully started +4ms
{
  status: 200,
  statusText: 'OK',
  headers: {
    date: 'Mon, 20 Apr 2020 05:16:44 GMT',
    'content-type': 'application/json',
    'content-length': '537',
    connection: 'close',
    server: 'gunicorn/19.9.0',
    'access-control-allow-origin': '*',
    'access-control-allow-credentials': 'true'
  },
  config: {
    url: 'http://httpbin.org/post',
    method: 'post',
    data: '{"contents":"test"}',
    headers: {
      Accept: 'application/json, text/plain, */*',
      'Content-Type': 'application/json;charset=utf-8',
      'User-Agent': 'axios/0.19.2',
      'Content-Length': 19
    },
    auth: { username: 'user name', password: 'password' },
    transformRequest: [ [Function: transformRequest] ],
    transformResponse: [ [Function: transformResponse] ],
    timeout: 0,
    adapter: [Function: httpAdapter],
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
    validateStatus: [Function: validateStatus],
    cancelToken: CancelToken { promise: [Promise], reason: [Cancel] }
  },
  request: ClientRequest {
    _events: [Object: null prototype] {
      socket: [Function],
      abort: [Function],
      aborted: [Function],
      error: [Function],
      timeout: [Function],
      prefinish: [Function: requestOnPrefinish]
    },
    _eventsCount: 6,
    _maxListeners: undefined,
    outputData: [],
    outputSize: 0,
    writable: true,
    _last: true,
    chunkedEncoding: false,
    shouldKeepAlive: false,
    useChunkedEncodingByDefault: true,
    sendDate: false,
    _removedConnection: false,
    _removedContLen: false,
    _removedTE: false,
    _contentLength: null,
    _hasBody: true,
    _trailer: '',
    finished: true,
    _headerSent: true,
    socket: Socket {
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: 'httpbin.org',
      _readableState: [ReadableState],
      readable: true,
      _events: [Object: null prototype],
      _eventsCount: 6,
      _maxListeners: undefined,
      _writableState: [WritableState],
      writable: false,
      allowHalfOpen: false,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: null,
      _server: null,
      parser: null,
      _httpMessage: [Circular],
      [Symbol(asyncId)]: 15,
      [Symbol(kHandle)]: [TCP],
      [Symbol(kSetNoDelay)]: false,
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: null,
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(kCapture)]: false,
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0
    },
    connection: Socket {
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: 'httpbin.org',
      _readableState: [ReadableState],
      readable: true,
      _events: [Object: null prototype],
      _eventsCount: 6,
      _maxListeners: undefined,
      _writableState: [WritableState],
      writable: false,
      allowHalfOpen: false,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: null,
      _server: null,
      parser: null,
      _httpMessage: [Circular],
      [Symbol(asyncId)]: 15,
      [Symbol(kHandle)]: [TCP],
      [Symbol(kSetNoDelay)]: false,
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: null,
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(kCapture)]: false,
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0
    },
    _header: 'POST /post HTTP/1.1\r\n' +
      'Accept: application/json, text/plain, */*\r\n' +
      'Content-Type: application/json;charset=utf-8\r\n' +
      'User-Agent: axios/0.19.2\r\n' +
      'Content-Length: 19\r\n' +
      'Host: httpbin.org\r\n' +
      'Authorization: Basic dXNlciBuYW1lOnBhc3N3b3Jk\r\n' +
      'Connection: close\r\n' +
      '\r\n',
    _onPendingData: [Function: noopPendingOutput],
    agent: Agent {
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      defaultPort: 80,
      protocol: 'http:',
      options: [Object],
      requests: {},
      sockets: [Object],
      freeSockets: {},
      keepAliveMsecs: 1000,
      keepAlive: false,
      maxSockets: Infinity,
      maxFreeSockets: 256,
      [Symbol(kCapture)]: false
    },
    socketPath: undefined,
    method: 'POST',
    insecureHTTPParser: undefined,
    path: '/post',
    _ended: true,
    res: IncomingMessage {
      _readableState: [ReadableState],
      readable: false,
      _events: [Object: null prototype],
      _eventsCount: 3,
      _maxListeners: undefined,
      socket: [Socket],
      connection: [Socket],
      httpVersionMajor: 1,
      httpVersionMinor: 1,
      httpVersion: '1.1',
      complete: true,
      headers: [Object],
      rawHeaders: [Array],
      trailers: {},
      rawTrailers: [],
      aborted: false,
      upgrade: false,
      url: '',
      method: null,
      statusCode: 200,
      statusMessage: 'OK',
      client: [Socket],
      _consuming: true,
      _dumped: false,
      req: [Circular],
      responseUrl: 'http://user%20name:password@httpbin.org/post',
      redirects: [],
      [Symbol(kCapture)]: false
    },
    aborted: false,
    timeoutCb: null,
    upgradeOrConnect: false,
    parser: null,
    maxHeadersCount: null,
    reusedSocket: false,
    _redirectable: Writable {
      _writableState: [WritableState],
      writable: true,
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      _options: [Object],
      _redirectCount: 0,
      _redirects: [],
      _requestBodyLength: 19,
      _requestBodyBuffers: [],
      _onNativeResponse: [Function],
      _currentRequest: [Circular],
      _currentUrl: 'http://user%20name:password@httpbin.org/post',
      [Symbol(kCapture)]: false
    },
    [Symbol(kCapture)]: false,
    [Symbol(kNeedDrain)]: false,
    [Symbol(corked)]: 0,
    [Symbol(kOutHeaders)]: [Object: null prototype] {
      accept: [Array],
      'content-type': [Array],
      'user-agent': [Array],
      'content-length': [Array],
      host: [Array],
      authorization: [Array]
    }
  },
  data: {
    args: {},
    data: '{"contents":"test"}',
    files: {},
    form: {},
    headers: {
      Accept: 'application/json, text/plain, */*',
      Authorization: 'Basic dXNlciBuYW1lOnBhc3N3b3Jk',
      'Content-Length': '19',
      'Content-Type': 'application/json;charset=utf-8',
      Host: 'httpbin.org',
      'User-Agent': 'axios/0.19.2',
      'X-Amzn-Trace-Id': 'Root=1-5e9d303c-3f9f4b3c5621034ae617c097'
    },
    json: { contents: 'test' },
    origin: 'my ip address',
    url: 'http://httpbin.org/post'
  }
}
```