Angular7 で俺式 MEAN スタック Webアプリを構築する、ほぼ全手順（3）

## 概要
Angular7 で俺式 MEAN スタックを作るための備忘録。
今回は「サーバ側の作成」を行う。

### 前提
2019年1月1日時点の情報です。また、以下の環境になっている前提です。

- Angular CLI： 7.0.6
- Node.js： 10.15.0
- npm： 6.4.1

また、「[Angular7 で俺式 MEAN スタック Webアプリを構築する、ほぼ全手順（2）](https://qiita.com/tomy0610/items/ae9092f933c399ec1bfa)」が完了していること。



## サーバサイド導入準備

### 型定義ファイルと関連モジュールをインストール

Universal 対応で既に導入されたモジュールの型定義ファイルと関連モジュールを入れておく

```bash
npm i @nguniversal/express-engine @nguniversal/module-map-ngfactory-loader

npm i -D @types/express
```

### ディレクトリを作成

以下の通りに、server ディレクトリ配下を作成しておく

```bash
［ルートディレクトリ］
  :
  └─ server # 新規作成
     ├─ constants # 定数用
     │   ├─ api-routes.constant.ts
     │   └─ http-method.constant.ts
     ├─ controllers # コントローラ処理用
     │   └─ sample.controller.ts
     ├─ managers # 管理系クラス用
     │   └─ api.manager.ts
     │
     ├─ tsconfig.json # TS 設定ファイル
     └─ web-server.ts # Web サーバ
```

### tsconfig ファイル作成

server/tsconfig.json の中身を以下の通りにする。

~~ついでに、各ファイルの import 時に `../` の多用を防ぐため、ルートディレクトリ直下の tsconfig.json にエイリアスを設定しておく。（common ディレクトリの部分）~~
↑なんかうまくいかないので、サーバサイドは一旦普通に相対パスで書くことにする。

```json-doc:server/tsconfig.json
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist/out-tsc",
    "module": "commonjs",
    "target": "es6",
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts"
  ]
}

```

## サーバサイド作成

### コントローラクラス作成

以下の通りにコントローラ作成

```ts:server/controllers/sample.controller.ts
import { ISamplePathParams, ISampleRequest, ISampleResponse } from '../../common/apis/sample/sample.api';

/** サンプル コントローラ */
export class SampleController {

  public async getUsers(request: ISampleRequest, params?: ISamplePathParams): Promise<ISampleResponse> {

    console.log('=== SampleContoroller #getUsers ===');
    console.log('params: ', params);
    console.log('request: ', request);

    const response: ISampleResponse = {
      users: [{
        id: 1, name: 'GET で API が呼ばれました', age: 11
      }]
    };

    return response;
  }

  public async createUser(request: ISampleRequest): Promise<ISampleResponse> {

    console.log('=== SampleContoroller #createUser ===');
    console.log('request: ', request);

    const response: ISampleResponse = {
      users: [{
        id: 2, name: 'POST で API が呼ばれました', age: 22
      }]
    };

    return response;
  }

  public async updateUser(request: ISampleRequest, params?: ISamplePathParams): Promise<ISampleResponse> {

    console.log('=== SampleContoroller #updateUser ===');
    console.log('params: ', params);
    console.log('request: ', request);

    const response: ISampleResponse = {
      users: [{
        id: 3, name: 'PUT で API が呼ばれました', age: 33
      }]
    };

    return response;
  }

  public async deleteUser(request: ISampleRequest, params?: ISamplePathParams): Promise<ISampleResponse> {

    console.log('=== SampleContoroller #deleteUser ===');
    console.log('params: ', params);
    console.log('request: ', request);

    const response: ISampleResponse = {
      users: [{
        id: 3, name: 'PUT で API が呼ばれました', age: 33
      }]
    };

    return response;
  }

}

```

### 定数クラス作成（HTTP メソッド定数）

以下の通りに HTTP メソッド定数作成

```ts:server/constants/http-method.constant.ts
/** HTTP メソッド定数 */
export enum HttpMethod {
  GET = 'GET',
  POST = 'POST',
  PUT = 'PUT',
  PATCH = 'PATCH',
  DELETE = 'DELETE'
}

```

### 定数クラス作成（API ルート定数）

以下の通りに API ルート定数作成

```ts:server/constants/api-routes.constant.ts
import { HttpMethod } from './http-method.constant';
import { SampleController } from '../controllers/sample.controller';
import { SAMPLE_API_PATH } from '../../common/apis/sample/sample.api';

/** API ルータ一覧 */
export const apiRoutes = [
  { path: SAMPLE_API_PATH, method: HttpMethod.GET, proccess: new SampleController().getUsers },
  { path: `${SAMPLE_API_PATH}/:id`, method: HttpMethod.GET, proccess: new SampleController().getUsers },
  { path: SAMPLE_API_PATH, method: HttpMethod.POST, proccess: new SampleController().createUser },
  { path: `${SAMPLE_API_PATH}/:id`, method: HttpMethod.PUT, proccess: new SampleController().updateUser },
  { path: `${SAMPLE_API_PATH}/:id`, method: HttpMethod.DELETE, proccess: new SampleController().deleteUser },
];

```

### API ルーティング管理クラスを作成

以下の通りに Express のルーティングを管理するクラスを作成

```ts:server/managers/api.manager.ts
import { Express, Request, Response, NextFunction } from 'express';

import { apiRoutes } from '../constants/api-routes.constant';
import { HttpMethod } from '../constants/http-method.constant';

/** API 管理クラス */
export class ApiManager {

  /** 各 API ルータを束ねたミドルウェアを生成 */
  public serve(api: Express) {

    for (const route of apiRoutes) {
      if (route.method === HttpMethod.GET) {
        api.get(route.path, this.proccess(route.proccess));
      } else if (route.method === HttpMethod.POST) {
        api.post(route.path, this.proccess(route.proccess));
      } else if (route.method === HttpMethod.PUT) {
        api.put(route.path, this.proccess(route.proccess));
      } else if (route.method === HttpMethod.DELETE) {
        api.delete(route.path, this.proccess(route.proccess));
      }
    }

  }

  /** API コントローラ呼出 */
  private proccess(controller: (requestData: any, requestParams: any) => Promise<any>) {

    return async (apiReq: Request, apiRes: Response, next: NextFunction) => {
      const httpMethod = apiReq.method;
      const params = apiReq.params;
      const request = (httpMethod === HttpMethod.GET || httpMethod === HttpMethod.DELETE) ? apiReq.query : apiReq.body;

      try {
        const response = await controller(request, params);
        apiRes.status(200).send(response);
      } catch (error) {
        apiRes.status(500).send(error);
      }

    };
  }

}

```

### Web サーバ作成

```ts:server/web-server.ts
// 以下の２インポートは最初に呼ばれる必要がある
import 'zone.js/dist/zone-node';
import 'reflect-metadata';

import * as express from 'express';
import * as bodyParser from 'body-parser';
import { join } from 'path';
import { ngExpressEngine } from '@nguniversal/express-engine';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
import { ApiManager } from './managers/api.manager';

const webServer = express();
const PORT = process.env.PORT || 3000;
const CLIENT_DIST_PATH = join(process.cwd(), 'dist/demo-app'); // TODO: パスをアプリ名に合わせる
// 動的に生成される dist ファイルため、require() のままにしておく
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require(join(process.cwd(), 'dist/demo-app-server/main')); // TODO: パスをアプリ名に合わせる

// テンプレートファイル名で拡張子が省略された場合のデフォルト拡張子を設定
webServer.set('view engine', 'html');
// テンプレートファイルを置くパスを設定
webServer.set('views', CLIENT_DIST_PATH);
// レンダリングに利用するテンプレートエンジンに Angular Express エンジン を登録
webServer.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [ provideModuleMap(LAZY_MODULE_MAP) ]
}));
// サーバサイドレンダリングされる静的ファイル
webServer.get('*.*', express.static(CLIENT_DIST_PATH));

// HTTP ボディ JSON 送信対応
webServer.use(bodyParser.urlencoded({ extended: true }));
webServer.use(bodyParser.json({ limit: '100kb' })); // Limit オプションでリクエストボディサイズ設定 (デフォルト: 100KB)
// API ルーティング提供
new ApiManager().serve(webServer);

// テンプレート SSR: 全ての通常ルートは Universal エンジンを使用
webServer.get('*', (req, res) => res.render('index', { req, res }));

// Node サーバ開始
webServer.listen(PORT, () => {
  console.log(`Node server is listening - http://localhost:${PORT}`);
});

```

`demo-app` の部分は、必要に応じて各自のアプリ名に書き換えてください。

## 起動

### 起動スクリプト修正

以下のとおりに、package.json のスクリプトを修正

```ts:package.json
:
  "scripts": {
    "dev": "ng serve",
    "start": "node ./dist/server/web-server.js",
    "start:npx": "npx node-static ./dist/demo-app --spa --port=3000", // 基本使わないけど入れておく
    "build": "npm run build:client && npm run build:server",
    "build:client": "ng run demo-app:app-shell:production",
    "build:clientsub": "ng build && ng run demo-app:server", // 基本使わないけど入れておく
    "build:server": "tsc --project server --outDir dist --allowjs true",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
:
```

### 確認

以下のコマンドでビルド、アプリ起動

```bash
# ビルド
npm run build

# 起動
npm run start
```

- ブラウザで `http://localhost:3000` にアクセスし、画面位表示されたボタンをクリック
- ブラウザコンソール、ターミナルコンソールに、それぞれ、クライアントとサーバの標準出力がなされればOK.
- ビルド完了後、以下のディレクトリと中身がルートディレクトリ配下に生成されていればOK.
 - dist/common（クラサバ共用ファイル群）
 - dist/demo-app（クライアントのソースコード）
 - dist/demo-app-server（Universal 対応で作ったやつ）
 - dist/server（Express および API ロジック用ファイル群）

### この後の手順

サーバサイド作らないと動作確認できないので、サーバサイドを作る。

👉 [次の開発手順はこちら](https://qiita.com/tomy0610/items/c7a8c5a3092c413e6269)