ExpressoTSで簡易APIサーバを作ってみる

簡易APIサーバ構築については、ExpressJSを使用したサンプルコードをネット上で見つけることができます。
ExpressJSですと実装するときのコードは概ねJavaScriptベースになります。
私自身、TypeScriptのほうがなじみがあるので簡易APIサーバーを構築するフレームワークがないかと探していたところ、ExpressoTSというのがリリースされているこを見つけました。
今回は、ExpressoTSで簡易APIサーバを作ってみたのでその情報を紹介したいと思います。

## ExpressoTS公式ページ

https://expresso-ts.com/


## 開発環境
* OS: Windows10
* node: 18.17.0
* ExpressoTS: ver2.6.0
* adapter-express: ver1.1.0

その他コマンドラインからビルドなどを実行する時はGitBashを使用します。標準で用意されるnpmコマンドラインにshellコマンドが含まれてるためです。


## 実装概要
* ExpressoTSをnpmコマンドを使用し、globalの環境にインストールします。これで以下のexpressotsコマンドが使用できるようになります。
* ExpressoTSのCLI expressotsコマンドを使用し、初期のプロジェクト（スケルトン）を自動生成します。
* 初期生成されたコードからGetの動作確認をします。初期プロジェクトにhttp Getメソッドは実装済みのため。
* つづいて、Postメソッドを実装します。ここでは、postリクエスト（Body部：Jsonデータ）の受取から、ビジネスロジック部分を呼び出し、シンプルなJSON形式のデータを返却するというものです。

## ソースコード
今回検証したソースコードはこちらに置いていますので参考にしていただけると幸いです。

https://github.com/qiita-mobileapps/simple-expressots-get-post

## プロジェクトの準備
### ExpressoTSインストール
```shell-session
$ npm i -g @expressots/cli
```
### 初期プロジェクト作成
```shell-session
$ expressots new simple-expressots-get-post

[🐎 Expressots]

? Project name (simple-expressots-get-post)  ※ エンターを押します
? Package manager (Use arrow keys)           ※ エンターを押します
> npm
  yarn
  pnpm
  bun
? Select a template (Use arrow keys)         ※ エンターを押します
> Non-Opinionated :: A simple ExpressoTS project.
  Opinionated :: A complete ExpressoTS project with an opinionated structure and features.
? Do you want to create this project? (Y/n)  ※ エンターを押します
※ プロジェクトの生成が開始されます。

Progress |████████████████████░░░░░░░░░░░░░░░░░░░░| 50% || Installing dependencies
      🐎 Project  simple-expressots-get-post created successfully!
      🤙 Run the following commands to start the project:
      $ cd simple-expressots-get-post
      $ npm run dev
      Happy coding!
      Please consider donating to support the project.
      💖 Sponsor: https://github.com/sponsors/expressots
※ ここまで表示されれば初期プロジェクトの生成の完了です。
```

### 生成されたディレクトリ構造
node_modules以外のディレクトリ内容は以下です。
```shell-session
$ tree -I node_modules
.
|-- README.md
|-- expressots.config.ts
|-- jest.config.ts
|-- package-lock.json
|-- package.json
|-- src
|   |-- app.container.ts
|   |-- app.controller.ts
|   |-- app.module.ts
|   |-- app.usecase.ts
|   `-- main.ts
|-- test
|   `-- app.usecase.spec.ts
|-- tsconfig.build.json
`-- tsconfig.json

```

### 生成された各ファイルの説明
これからGetとPostメソッドについて確認していきますが、その前に主要なファイルについて説明します。
今回のGetメソッドを確認する際に「app.controller.ts」と「app.usecase.ts」ファイルを主に参照します。
| ファイル名 | 概要 |
|:-:|:-|
| app.container.ts  |  アプリケーションの依存関係を記述 |
| app.controller.ts  | HTTPリクエストを処理するためのコードを記述  |
| app.module.ts  |  アプリケーションの各部分を組み立て、結合を記述 |
| app.usecase.ts  | アプリケーションのビジネスロジックを記述  |
|  main.ts | アプリケーションのエントリーポイントとなるファイル  |

### サービスの起動
サービスを起動します。
```shell-session
$ cd simple-expressots-get-post
$ npm run dev

> simple-expressots-get-post@1.0.0 dev
> tsnd ./src/main.ts

[INFO] 12:35:21 ts-node-dev ver. 2.0.0 (using ts-node ver. 10.9.1, typescript ver. 4.9.5)
Application version not provided is running on port 3000 - Environment: development
※ ここまで表示されれば正常に起動しています。3000ポートを使用します。
```
続いて、ソースコードの内容をみていきます。

## Getメソッド
Getメソッドは、上記初期プロジェクトが生成されるとGetメソッドはすでに実装がされています。
実装されているソースコードを参照します。

続いて、リクエストをcurlコマンドにより送信します。
すでに実装されているGetメソッドの応答結果「Hello from ExpressoTS!」が得られます。
```shell-session
$ curl localhost:3000
Hello from ExpressoTS!
※ メッセージが帰ってくれば正常に動作しています。
```

### Getメソッドの実装（入り部口部分）の確認
app.controller.tsにGetメソッドが実装されています。
```typescript
import { BaseController } from "@expressots/core";
import { Response } from "express";
import { AppUseCase } from "./app.usecase";
import { Get, controller, response } from "@expressots/adapter-express";

/*
* AppController クラスの定義
* ExpressotsのBaseControllerクラスを継承しています。
* これにより、Expressotsの機能を利用できるようになります。
*/
@controller("/")
export class AppController extends BaseController {
    constructor(private appUseCase: AppUseCase) {
        super();
    }

    /*
    * @Get("/") デコレータは、GETリクエストを処理するためのメソッドを定義しています。
    * このメソッドは / ルートに対するGETリクエストを処理します。
    * この定義により「localhost:3000」でアクセスするとメッセージが返却されます。
    */
    @Get("/")
    execute(@response() res: Response) {
        return res.send(this.appUseCase.execute());
    }
}
```
### Getメソッドの業務ロジックの確認
上記の```this.appUseCase.execute()```の実装内容を以下に示します。
上記curlコマンドによりリクエストを送付した結果「Hello from ExpressoTS!」メッセージが返却されました。
```typescript
import { provide } from "inversify-binding-decorators";

@provide(AppUseCase)
export class AppUseCase {
    /*
    * executeメソッドの戻り値に「Hello from ExpressoTS!」が設定されています。
    */
    execute() {
        return "Hello from ExpressoTS!";
    }
}
```
Getメソッドについて以上です。

## Postメソッド
Postメソッドは、初期のプロジェクトに実装がされていないため、追加実装します。
なお、ExpressoTSは@Get("/") デコレータをPostに変更するだけでは動作せず、その他プロバイダなどを実装する必要があります。
数ファイル追加しつつ、既存のコードに追加記述しながら進めます。

### Postメソッドのシナリオと実装概要
アクセスするURIパスは、```http://localhost:3000/user```とします。
リクエストにname,email項目を送信して、レスポンスにname,email,statusをセットし受け取ります。
POSTメソッドにおいて、JSON形式のリクエストBody部に名前と電子メールの項目、およびレスポンスには名前と電子メールに加え処理結果を返却する想定で実装をします。
なお、各ファイルを追加する際に```expressots generate```コマンドを使用することができます。
これからの作業はexpressots generateコマンドを使用しながら進めます。

### Postメソッド実装後のファイル群
初期で生成されたファイルの修正に加え、「app.provider.ts」および「user.dto.ts」を追加します。
```diff
$ tree -I node_modules
.
|-- README.md
|-- expressots.config.ts
|-- jest.config.ts
|-- package-lock.json
|-- package.json
|-- src
|   |-- app.container.ts
|   |-- app.controller.ts
|   |-- app.module.ts
+|   |-- app.provider.ts
|   |-- app.usecase.ts
|   |-- main.ts
+|   `-- user.dto.ts
|-- test
|   `-- app.usecase.spec.ts
|-- tsconfig.build.json
`-- tsconfig.json
```

### Postメソッドの実装
以下に実装するファイルと詳細内容を説明します。

#### 1\. 送受信する項目の定義記述
**user.dto.ts**ファイルを用意します。
このファイルにJSON形式で送受信を行う項目定義をします。
entityもしくはdtoとして定義します。今回は、dtoとして定義しました。
user.dtoを以下のコマンドにより追加します。
```shell-session
$ expressots generate dto user2
[🐎 Expressots]
  [dto]          user dto created! ✔️
```
続いて内容を編集します。
以下の内容を記述します。
```typescript
export interface IUserRequestDTO {
  name: string;
  email: string;
}

export interface IUserResponseDTO {
  name: string;
  email: string;
  status: string;
}
```
#### 2\. POSTメソッドから呼び出される業務ロジック部分の記述
続いて、**app.usecase.ts**ファイルを編集します。
このファイルは、すでにGetメソッドから呼び出されるexecute()メソッドが実装されています。
今回は、このファイルにPostメソッドで使用するexecutePostを追加で記述します。
ここで追加したexecutePostをControllerのPostデコーダ内の処理から呼び出します。
```diff_typescript
import { provide } from "inversify-binding-decorators";
import { IUserRequestDTO, IUserResponseDTO } from "./user.dto"

@provide(AppUseCase)
export class AppUseCase {
    execute() {
        return "Hello from ExpressoTS!";
    }

+    executePost(data: IUserRequestDTO): IUserResponseDTO {
+        return {
+            name: data.name,
+            email: data.email,
+            status: "Success"
+        };
+    }
}

```

#### 3\. POSTデコーダ部分の記述
**app.controller.ts**ファイルに追加記述します。
```diff_typescript
import { BaseController, StatusCode } from "@expressots/core";
import { Response } from "express";
import { AppUseCase } from "./app.usecase";
import { Get, Post, body, controller, response } from "@expressots/adapter-express";
import { IUserRequestDTO, IUserResponseDTO } from "./user.dto"

@controller("/")
export class AppController extends BaseController {
    constructor(private appUseCase: AppUseCase) {
        super();
    }

    // こちらは自動生成されたGetメソッドの値の返却処理
    @Get("/")
    execute(@response() res: Response) {
        return res.send(this.appUseCase.execute());
    }

    /*
    * @Postを追加して、usreパスにアクセスします。
    * executePost()を呼び出し業務処理を行います。
    * 今回はリクエスト内容に処理結果を追加したJSONデータを返却する実装にします。
    * BaseControllerより継承されたcallUseCaseを呼び出しデータを返却し、処理を終了します。
    */
+    @Post("user")
+    async executePost(
+        @body() payload: IUserRequestDTO,
+        @response() res: Response
+    ): Promise<IUserResponseDTO> {
+        console.log("body: ", payload);
+        const result: IUserResponseDTO = this.appUseCase.executePost(payload);
+        return this.callUseCase(
+            result,
+            res,
+            StatusCode.Created,
+        );
+    }
}
```

#### 4\. Body部のパース処理の記述
**app.provider.ts**ファイルを追加します。
以下の実装によりリクエストに含まれるBody部のデータを内部的に扱うことができるようになります。
```shell-session
$ expressots generate provider app
[🐎 Expressots]
  [provider]          app provider created! ✔️
```
**app.provider.ts**ファイルを開き以下の内容を記述します。

```diff_typescript
/*
* @expressots/coreに定義されている、IMiddleware, Middlewareを読み込みます。
*/
import { provide } from "inversify-binding-decorators";
+ import { IMiddleware, Middleware } from "@expressots/core";
+ import { AppExpress } from "@expressots/adapter-express";
+ import { container } from "./app.container";

@provide(App)
export class App extends AppExpress {
+    private middleware: IMiddleware;
    /*
    * MiddlewareのaddBodyParserメソッドにてBody部のパースをするので、
    * 必要なモジュール類を用意します。
    */
+    constructor() {
+        super();
+        this.middleware = container.get<IMiddleware>(Middleware);
+    }
+    protected configureServices(): void {
+        this.middleware.addBodyParser();
+    }
}
```

#### 5\. Providerの有効化
このファイルが最後の編集ファイルです。

上記Providerを追加しましたので、AppFactoryに認識させます。
**main.ts**ファイルを開きます。
上記で追加した```app.provider```を認識させます。
```diff_typescript
import "reflect-metadata";

import { AppFactory } from "@expressots/core";
import { container } from "./app.container";
import { ServerEnvironment } from "@expressots/adapter-express";
+ import { App } from "./app.provider";

async function bootstrap() {
-    const app = await AppFactory.create(container, []);
+    const app = await AppFactory.create(container, App);
    await app.listen(3000, ServerEnvironment.Development);
}

bootstrap();
```
#### Postメソッドの動作確認
curlコマンドからPOSTとして、```http://localhost:3000/user```リクエスト送信が正しく動作するか確認します。
{"name":"aaa2","email":"aaa2@bbb","status":"Success"}が返却されれば正常に操作しています。
```shell
$ curl -X POST -H "Content-Type: application/json" \
-d '{"name": "aaa2", "email": "aaa2@bbb"}' \
http://localhost:3000/user
{"name":"aaa2","email":"aaa2@bbb","status":"Success"}
```

Postメソッドについては以上です。

## 補足
上記の初期プロジェクト作成時の手順で```Non-Opinionated```を```Opinionated```として生成すると、各種IOが含まるプロジェクトが生成されます。Non-Opinionatedでは基本的にはGetメソッドのみで、各資材やモジュールの配置もシンプルでした。
Opinionatedを指定して展開するとストレージ間I/O（参照、更新、削除処理）など各機能が満載なプロジェクトが生成されます。
興味のある方はお試しくださいませ。
```
? Select a template (Use arrow keys)         ※ 2つ目のOpinionatedを選択する
  Non-Opinionated :: A simple ExpressoTS project.
> Opinionated :: A complete ExpressoTS project with an opinionated structure and features.
```

## さいごに
今回はExpressoTSで簡易APIサーバを作ってみるということで実装した内容を紹介しました。
次回は、ExpressoTSのバックエンドのなかでデータベースへの書き込み処理について実装してみたいと思います。