OvernightJSでNode+Express+TypeScriptなAPIサーバーを構築する

日本語情報が全く見当たらないExpressのTypeScript decoratorである[OvernightJS](https://github.com/seanpmaxwell/overnight)を使ってみたので、導入方法をざっくりまとめます。

Node/ExpressでTypeScriptを使う方法については、簡単に書きはしますが本筋ではないので詳細は他を当たってください。[このあたりとか](https://qiita.com/notakaos/items/3bbd2293e2ff286d9f49)参考になります。

# 前提環境

- Node 12.18.4
- Express 4.17.1
- TypeScript 4.0.3

開発環境でのTypeScriptの実行には[ts-node](https://github.com/TypeStrong/ts-node)と[ts-node-dev](https://github.com/whitecolor/ts-node-dev)を使います。ts-node-devはコード修正するとホットリロードしてくれるのでとても楽。

# セットアップ

```
$ npm init
　プロジェクトの設定をよしなに
$ npm install express @overnightjs/core @overnightjs/logger body-parser cors http-status-codes
$ npm install -D typescript npm-run-all rimraf ts-node ts-node-dev @types/node@12 @types/express @types/cors
$ npx tsc --init
```

tsconfig.jsonが生成されるので、その中にある`target`を`ES2019`に、`esModuleInterop`と`experimentalDecorators`と`emitDecoratorMetadata`のコメントを外してtrueに設定します。

package.jsonの`scripts`部分はこんな感じにしました。ここはOvernightJS固有の要素は特になくTypeScript使う場合の一般的な設定内容です。開発環境ではts-nodeやts-node-devでTypeScriptファイルを直接実行、本番環境ではトランスパイルします。

```json:package.json
  "scripts": {
    "dev": "ts-node ./src/index.ts",
    "dev:watch": "ts-node-dev ./src/index.ts",
    "build": "npm-run-all clean tsc",
    "clean": "rimraf dist/*",
    "tsc": "tsc",
    "start": "node ."
  },
```

余談ですがトランスパイルして実行するのとts-nodeで直接実行するのでパフォーマンス大差ないという話もあるので、要件がシビアでなければ本番もts-node運用でもいいかもしれません。

# 実装
大雑把にコントローラーとサーバーの二段構成です。型安全過激派なので徹底的に型で縛ります。
## コントローラー
`@Controller`というアノテーションを付けたクラスがコントローラーと認識されます。パラメーターはエンドポイントのパスで、サーバーがlocalhostの3000番ポートで動いているとすると以下の例では`http://localhost:3000/api/songs`がエンドポイントになります。

各HTTPメソッドに対応するアノテーション`@Get`、`@Post`、`@Put`、`@Delete`が用意されていて、リクエストを受けるとリクエストメソッドに対応するアノテーションが付いた関数が呼び出されます。`@Get(':id')`のようにコロンで始まる文字列を渡した場合、`req.params.id`でパラメーターを受け取ることができます。`http://localhost:3000/api/songs/1`にGETリクエストを送ると`req.params.id`は1になります。

```typescript:controller/SongController.ts
import { Request, Response } from 'express';
import { Controller, Get, Post, Put, Delete } from '@overnightjs/core';
import { Logger } from '@overnightjs/logger';
import { StatusCodes } from 'http-status-codes';

interface ISong {
  id: number;
  title: string;
  artist: string;
}

interface ISongGetRequestParams {
  id: number;
}

interface ISongUpdateRequestParams {
  id: number;
}

interface ISongDeleteRequestParams {
  id: number;
}

@Controller('api/songs')
export class SongController {
  @Get('')
  private async getAll(req: Request<void, ISong[], void, void>, res: Response<ISong[]>): Promise<Response<ISong[]>> {
    // ほんとはDBとかからデータ取得する
    const songs: ISong[] = [
      {id: 1, title: 'So Sweet', artist: '諏訪ななか'},
      {id: 2, title: 'クローバー', artist: '楠木ともり'}
    ];

    return res.status(StatusCodes.OK).json(songs); 
  }

  @Get(':id')
  private async get(req: Request<ISongGetRequestParams, ISong, void, void>, res: Response<ISong>): Promise<Response<ISong>> {
    Logger.Info(req.params, true);

    // ほんとはDBとかからデータ取得する
    const song: ISong = {
      id: 1,
      title: 'So Sweet',
      artist: '諏訪ななか'
    };

    return res.status(StatusCodes.OK).json(song);
  }

  @Post()
  private async add(req: Request<void, void, ISong, void>, res: Response<void>): Promise<response<void>> {
    Logger.Info(req.body, true);

    const song: ISong = req.body;

    // ほんとはDBとかにデータ追加する

    return res.status(StatusCodes.OK).json();
  }

  @Put(':id')
  private async update(req: Request<ISongUpdateRequestParams, void, ISong, void>, res: Response<void>): Promise<Response<void>> {
    Logger.Info(req.body, true);

    const song: ISong = req.body;

    // ほんとはDBとか更新する

    return res.status(StatusCodes.OK).json();
  }

  @Delete(':id')
  private async delete(req: Request<ISongDeleteRequestParams, void, void, void>, res: Request<void>): Promise<Response<void>> {
    Logger.Info(req.params, true);

    const id: number = req.params.id;

    // ほんとはDBとかからデータ削除する

    return res.status(StatusCodes.OK).json();
  }
}
```

## サーバー
Expressに対してあれこれ設定して、コントローラーをインスタンス化してOvernightJSに登録して待ち受けを開始します。

ここでは`this.app`がExpressのインスタンスなので、普通にExpressを使うときと同様にミドルウェア登録したりとかいろいろできます。OvernightJSの`Server`がExpressをラップしてる感じですね。

```typescript:SongServer.ts
import * as bodyParser from 'body-parser';
import cors from 'cors';
import { Server } from '@overnightjs/core';
import { Logger } from '@overnightjs/logger';
import { SongController } from './controller/SongController';

class SongServer extends Server {
  constructor() {
    super(process.env.NODE_ENV === 'development');
    this.app.use(bodyParser.json());
    this.app.use(bodyParser.urlencoded({extended: true}));
    this.app.use(cors());
    this.setupControllers();
  }

  private setupControllers(): void {
    const songController: SongController = new SongController();
    super.addControllers([songController]);
  }

  public start(port: number): void {
    this.app.listen(port, (): void => {
      Logger.Imp(`server listening on port ${port}`);
    });
  }
}

export default SongServer;
```

## index.ts
エントリーポイントですが、やることはServerを叩くだけです。

```typescript:index.ts
import SongServer from './SongServer';

const server: SongServer = new SongServer();
server.start(3000);
```

# 起動

```
$ npm run dev:watch
```

これでホットリロードありでサーバーが起動します。

# おわりに

TypeScriptでExpressのアプリケーション作る場合は`express-generator`で生成したJavaScript一式をTypeScriptに書き直すという苦行も1つの手段としてありますが、OvernightJSのようなデコレーターを使うとちゃんとTypeScriptらしい書き方で、かつシンプルにREST APIを実装できます。凝ったことしなければRouterとか意識する必要すらないです（今回は触れてないですが、複雑な要件のためにOvernightJSはMiddlewareやRouterを扱う機能も持っています）。

Express+TypeScriptの環境でAPIサーバーをさくっと作りたい方、検討してみるとよいかと思います。