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

by dicekest,

Deno

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

作るもの

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

全体がみたい方はこちら

https://github.com/rydein/deno_first_api

インストールとか下準備

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

$ brew install deno

エディタはVSCodeを使いますがdenoのimport記法に対応させるためこちらのプラグインを追加しましょう。

Docker + docker-compose

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

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"]
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をたちあげておきたいので動く環境だけ用意します

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();

立ち上げます

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

DB migration

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

https://deno.land/x/nessie

init

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

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

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の部分は作成するテーブルによって適宜読み替えてください。

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

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

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を実行しましょう。

#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に登録、取得する処理を作成します。

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のルーティングを作成してリクエストを処理していきます。

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配下に上記で作成したルーティングが用意されます。

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ぐらいで小さいので良さそうですね、次はクラウド環境にデプロイしてみたいと思います。