Deno+MySQL+DockerでAPIサーバーをさくっと作る


## Deno

TypeScriptを標準サポートしている[Deno](https://deno.land/)のv1.0がリリースされたということで興味があったので触ってみました。
あと恐竜がかわいい。

## 作るもの

deno + MySQL + docker-composeでローカル環境にシンプルなAPIサーバーです。
適当にどう森の村民を登録して編集するようなやつとします。

全体がみたい方はこちら

https://github.com/rydein/deno_first_api

## インストールとか下準備

公式に書いてある通りですが以下のコマンドを流すだけです

```
$ brew install deno
```

エディタはVSCodeを使いますがdenoのimport記法に対応させるためこちらの[プラグイン](https://marketplace.visualstudio.com/items?itemName=justjavac.vscode-deno)を追加しましょう。

<img width="628" alt="Screen Shot 2020-05-25 at 23.49.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236405/2777f365-d6b1-a1ab-29d0-cfad7cadb036.png">

## Docker + docker-compose

deno用のDockerfileとdocker-compose.ymlはこちらを使います

```yaml:Dockerfile
FROM hayd/ubuntu-deno:1.0.0

EXPOSE 3000

WORKDIR /app

USER deno

ADD . .

RUN deno cache index.ts

CMD ["run", "--allow-net", "index.ts"]
```

```yaml:docker-compose.yml
version: '3'

services:
  deno:
    build: .
    ports:
      - "3000:3000"

  db:
    image: mysql:8
    ports:
        - "3306:3306"    
    environment:
      - MYSQL_ROOT_PASSWORD=Passw0rd
      - MYSQL_DATABASE=deno-dev
    volumes:
      - db-volume:/var/lib/mysql
      - ./mysql/conf:/etc/mysql/conf.d

volumes:
  db-volume:
```

## ローカル環境立ち上げ

とりあえずMySQLをたちあげておきたいので動く環境だけ用意します

```javascript:index.ts
import { Application, Router } from "https://deno.land/x/denotrain@v0.5.0/mod.ts";

const app = new Application();
const router = new Router();

app.get("/", (ctx) => {
  return {"hello": "world"};
});
await app.run();
```

立ち上げます

```bash
$ docker-compose build
$ docker-compose up -d
```

## DB migration

まずはDBのテーブル定義を作成していきます。
migrationにはこちらのツールを使います。

https://deno.land/x/nessie

### init

```bash
# nessie.config.ts を作成してdb接続情報を追記する
$ deno run --allow-net --allow-read --allow-write https://deno.land/x/nessie/cli.ts init
```

MySQL以外のデフォルトコードが生成されますが今回は不要なので消してしまいましょう。
`configMySql`にDB接続情報を追記します。

```javascript:nessie.config.ts
const configMySql = {
  migrationFolder: `./migrations`,
  connection: {
    hostname: "localhost", // hostからDockerのMySQLコンテナに繋ぐ
    port: 3306,
    username: "root",
    password: "Passw0rd",
    db: "deno-dev",
  },
  dialect: "mysql",
};

export default configMySql;
```

接続情報が作成できたのでmigrationファイルを作成します。
`create_villagers`の部分は作成するテーブルによって適宜読み替えてください。

```bash
# 村民のmigration
$ deno run --allow-net --allow-read --allow-write https://deno.land/x/nessie/cli.ts make create_villagers
```

`migration/`ディレクトリにmigrationファイルが生成されるので必要な情報を追記していきます。
村民には名前、性別、性格、誕生日があるので追加します。

```javascript:migrations/1590336600981-create_villagers.ts
import { Schema } from "https://deno.land/x/nessie/mod.ts";

// migration ファイルにテーブル情報を追加する
export const up = (schema: Schema): void => {
    schema.create("villagers", (table) => {
        table.id();
        table.string("name", 100).nullable();        // 名前
        table.string("gender", 100).nullable();      // 性別
        table.string("personality", 100).nullable(); // 性格
        table.string("birthday", 100).nullable();    // 誕生日
    });
};

export const down = (schema: Schema): void => {
    schema.drop("villagers");
};
```

migrationを実行しましょう。

```bash
#migration の実行
$ deno run --allow-net --allow-read https://deno.land/x/nessie/cli.ts migrate
```


## Query Builder

DBからデータを参照したり登録したりするにはこちらのクエリビルダーを使います。

https://deno.land/x/dex


場所はどこでも良いですが`models/villagers.ts`にDBに登録、取得する処理を作成します。

```javascript:models/villagers.ts
import Dex from "https://deno.land/x/dex/mod.ts";
import client from "./config.ts";

let dex = Dex({client: "mysql"});
let table = "villagers";

interface Villager {
    id?: number,
    name: string,
    gender: string,
    personality: string,
    birthday: string
}

///
/// 新しい村民を追加して追加したデータを返す
///
function addVillager(villager: Villager) {
    const insertQuery = dex.queryBuilder().insert([villager]).into(table).toString();
    return client.execute(insertQuery).then((result: any) => {
        const getQuery = dex.queryBuilder().select().from(table).where({id: result.lastInsertId}).toString();
        return client.execute(getQuery).then((result: any) => result.rows ? result.rows[0] : {});
    })
}

///
/// 全ての村民を返す
///
function getAllVillagers() {
    const getQuery = dex.queryBuilder().select("*").from(table).toString();
    return client.execute(getQuery);
}

///
/// 村民の更新を行う、更新されたデータを返す
///
function editVillager(id: number, villager: Villager) {
    const editQuery = dex.queryBuilder().from(table).update(villager).where({id}).toString();
    return client.execute(editQuery).then(() => {
        const getQuery = dex.queryBuilder.select().from(table).where({id}).toString();
        return client.execute(getQuery).then((result: any) => result.rows ? result.rows[0] : {});
    });
}

///
/// 村民の削除
///
function deleteVillager(id: number) {
    const deleteQuery = dex.queryBuilder().from(table).delete().where({id}).toString();
    return client.execute(deleteQuery)
}

export {
    addVillager,
    getAllVillagers,
    editVillager,
    deleteVillager
}
```

## Http Server

https://deno.land/x/denotrain

`controllers/villagers.ts` にコントローラーを作成し、APIのルーティングを作成してリクエストを処理していきます。

```javascript:controllers/villagers.ts
import { Router } from "https://deno.land/x/denotrain@v0.4.4/mod.ts";
import { addVillager, getAllVillagers, editVillager, deleteVillager } from "../models/villagers.ts";

const api = new Router();

api.get("/", (ctx) => {
    return getAllVillagers().then((result: any) => {
        return result.rows;
    })
})

api.post("/", (ctx) => {
    const body = {
        name: ctx.req.body.make,
        gender: ctx.req.body.gender,
        personality: ctx.req.body.personality,
        birthday: ctx.req.body.birthday,
    }

    return addVillager(body).then((villager: any) => {
        ctx.res.setStatus(201);
        return villager;
    })
})

api.patch("/:id", (ctx) => {
    const body = {
        name: ctx.req.body.make,
        gender: ctx.req.body.gender,
        personality: ctx.req.body.personality,
        birthday: ctx.req.body.birthday,
    }

    return editVillager(ctx.req.params.id as number, body).then((result: any) => {
        return result;
    })
});

api.delete("/:id", ctx => {
    return deleteVillager(ctx.req.params.id as number).then(() => {
        ctx.res.setStatus(204);
        return true;
    })
});

export default api;
```

`index.ts`にサーバーのエントリーポイントを用意し、作成したRouterとURLをセットします。
`/api/villagers`に作成したRouterをセットすることで、このURL配下に上記で作成したルーティングが用意されます。

```javascript
import { Application, Router } from "https://deno.land/x/denotrain@v0.4.4/mod.ts";
import api from "./controllers/villagers.ts";

const app = new Application({});
app.use("/api/villagers", api);

app.run();
```

## 起動

サーバーの用意ができたので起動しましょう、以下のURLでAPIが起動しているはずです。
http://localhost:3000/

```
$ docker-compose build
$ docker-compose up -d
```

まずは最初のAPIを作ってみましたが、ライブラリを探すのも公式ページから検索できてドキュメントも読みやすくそろっているのでとくにハマることなく進みました。

Dockerのイメージも40MBぐらいで小さいので良さそうですね、次はクラウド環境にデプロイしてみたいと思います。