Deno/TypeScript入門〜スタブ・サーバーを作ってみる編〜

# まえがき

※この記事は、以下の後続記事です。
Deno/TypeScriptの概要については以下の記事に記載しています。

https://qiita.com/y-some/items/ca6404b7024d54b1ff75

---

最近私はツールを開発する環境として「[Deno](https://deno.land/)」が気に入っています。
シンプルで、軽くて、ランタイムAPIや標準ライブラリが充実していて、開発体験が良いと感じています。
私のところではDenoを本番で採用しようとすると諸々ハードルがありますが、ツールの開発であれば選択肢として大いにアリです。

iOS/Androidアプリケーションや、シングルページ・Webアプリケーションを開発する際、よく使うツールの一例として「スタブ・サーバー」があります。

特に、バックエンド側（Web API）とフロントエンド側が別チームだったりする場合に、
**「バックエンド側の実装が完了するまでの間、フロントエンド側の実装が停滞してしまう」**
といった状況が発生することがあります。

そのような場合、**ダミーのレスポンスを返す「スタブ・サーバー」があると開発がスムーズになります。**

この記事では、Deno/TypeScriptによるスタブ・サーバーのサンプルを解説します。
私自身も初学者なので、間違い等があればコメントでご教示ください。

# 前提環境

Deno 1.20.5

# Denoのインストール

公式ドキュメントを参照するのが確実です。

https://deno.land/manual/getting_started/installation

# Visual Studio Code (VSCode) の環境構築

テキストエディタでも開発は可能ですが、統合開発環境（IDE）の方が、コード補完や静的チェックが効きますので開発が捗ります。

Denoでは皆さんVSCodeを使っていらっしゃるようです。
以下の記事に素晴らしくまとまっています。

https://qiita.com/SuzuTomo2001/items/3d85eb92fcfff255ce46

あれこれ調べるのがめんどくさかったら（笑）、
拡張機能「Deno for Visual Studio Code」をインストールして、
作業フォルダ配下に`.vscode`フォルダを作成し、
以下のファイルを格納すれば、とりあえず開発を始められると思います。

```json: settings.json
{
    "debug.javascript.unmapMissingSources": true,
    "deno.enable": true,
    "deno.lint": true,
    "deno.unstable": true,
    "deno.suggest.imports.hosts": {
        "https://deno.land": true
    }
}
```

VSCodeのデバッグ実行を使いたい場合は`launch.json`も格納します。

<details><summary>launch.json</summary>

```json: launch.json
{
    "version": "0.2.0",
    "configurations": [
      {
        "name": "Deno",
        "type": "pwa-node",
        "request": "launch",
        "cwd": "${workspaceFolder}",
        "runtimeExecutable": "deno",
        "runtimeArgs": ["run", "--inspect-brk", "-A", "./app.ts"],
        "attachSimplePort": 9229
      }
    ]
}
```

※app.tsはアプリケーションのエントリーポイントとなるファイル名ですので、適宜書き変えてください。

</details>

# スタブ・サーバーの開発

## コード

```typescript: app.ts
import { Application, Router, Context, helpers } from "https://deno.land/x/oak@v10.5.1/mod.ts";

// 型定義
interface User {
  id: number;
  name: string;
  address: string | null;
}

// データ定義
const userA: User = {id: 1, name: "user-a", address: "東京都豊島区池袋"};
const userB: User = {id: 2, name: "user-b", address: null};
const userC: User = {id: 3, name: "user-c", address: "北海道札幌市北区"};
const users: User[] = [userA, userB, userC]

/* APIごとの関数定義 */

// 全取得用関数
const fetch = (context: Context) => {
  context.response.body = {"users": users};
}

// ID指定取得用関数
const find = (context: Context) => {
  const { id } = helpers.getQuery(context, { mergeParams: true });
  const foundUser = users.find(user => user.id == Number(id));
  if (foundUser) {
    context.response.body = foundUser;
  } else {
    context.response.status = 404;
    context.response.body = "Not found.";
  }
}

// 生成用関数
const create = async (context: Context) => {
  const { id, name, address } = await context.request.body().value;
  if (id && name) {
    const newUser: User = {id: id, name: name, address: address}
    users.push(newUser);
    context.response.body = "Success.";  
  } else {
    context.response.status = 403;
    context.response.body = "Bad parameter(s).";  
  }
}

/* アプリケーションのエントリーポイント */

const router = new Router();
router
  .get("/", (context) => {
    context.response.body = "Hello world!";
  })
  .get("/users", fetch)
  .get("/users/:id", find)
  .post("/users", create)
  ;

const app = new Application();
app.use(router.routes());
app.use(router.allowedMethods());

console.log("Stub-Server runs at 'http://localhost:8000/'");
await app.listen({ port: 8000 });
```

## 起動

Denoのセキュリティ機構としてネットワークを許可するための`--allow-net`オプションを付けて実行します。

```
$ deno run --allow-net app.ts
```

## 動作確認

[cURL](https://ja.wikipedia.org/wiki/CURL)などHTTPリクエストを投げるツールはいろいろあるかと思いますが、今回はVSCodeを使って開発しているので、その拡張機能「[REST Client](https://github.com/Huachao/vscode-restclient)」が便利です。

### GET

リクエスト：
![スクリーンショット 2022-05-13 8.27.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122917/f417fc13-a1d0-c2ab-821e-9527f86cc514.png)

レスポンス：
```
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
vary: Accept-Encoding
content-encoding: gzip
content-length: 146
date: Fri, 06 May 2022 02:52:06 GMT

{
  "users": [
    {
      "id": 1,
      "name": "user-a",
      "address": "東京都豊島区池袋"
    },
    {
      "id": 2,
      "name": "user-b",
      "address": null
    },
    {
      "id": 3,
      "name": "user-c",
      "address": "北海道札幌市北区"
    }
  ]
}
```

リクエスト：
![スクリーンショット 2022-05-13 8.28.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122917/dfb191e2-a062-0b4c-e154-fe3e259f19af.png)

レスポンス：
```
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
vary: Accept-Encoding
content-encoding: gzip
content-length: 84
date: Fri, 06 May 2022 02:55:01 GMT

{
  "id": 1,
  "name": "user-a",
  "address": "東京都豊島区池袋"
}
```

### POST

リクエスト：
![スクリーンショット 2022-05-13 8.30.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122917/ca8970e4-7c0e-1be3-0b3b-e3ca50b2d9b2.png)

レスポンス：
```
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
vary: Accept-Encoding
content-length: 8
date: Fri, 06 May 2022 03:09:24 GMT

Success.
```

※↑をPOSTした後のGETでは、ちゃんと`{"id": 1000, "name": "山田 太郎"}`も返ってきます。

## コードのちょっとした解説

### 「oak」について

oakはHTTPのルーティングと、Webアプリケーション・サーバーの役割を持つサードパーティ・ライブラリです。
外部モジュール（標準ライブラリやサードパーティ・ライブラリ）を`import`する際は`/oak@v10.5.1`のようにバージョン番号をpathで指定します。

oakの`Router`では、`"/:id"`のような形式でpathをパラメータとして扱うことができ、`helpers.getQuery`で取得できます。

### レスポンスデータの定義について

なんと言っても、静的な型を定義できることがTypeScriptの強みです。
型の間違いは、VSCodeでリアルタイムにチェックが働きます。

また、nullを許容するか否か (nullable) を宣言でき、IDEで静的なチェックが働きます。

参考リンク：
[TypeScript Deep Dive 日本語版](https://typescript-jp.gitbook.io/deep-dive/) > [TypeScriptの型システム](https://typescript-jp.gitbook.io/deep-dive/type-system)

# さらに詳細な情報を得られるお勧めリンク集

## Deno

https://deno.land/

https://zenn.dev/uki00a/books/effective-deno

## TypeScript

https://typescript-jp.gitbook.io/deep-dive/

## VSCode拡張「REST Client」

https://qiita.com/toshi0607/items/c4440d3fbfa72eac840c