カタベログ

IT技術に関するブログを書きたい.食べ物関連はInstagramをご参照の事.

Heroku上にdeno + deno-express + typescriptのAPIサーバを作ってみた

色々なことをサボってきてまもなく2020年が終わろうとしている。 少しでも今年のことは今年のうちに済ませないといけないと思うのが人間ってもんなので、1日しっかりとプログラミングしてみました。

最初はバックエンドはPythonで書こうと思ってたんですけど、実務に役立たないし、Pythonでバックエンド書くメリットを見失ったのでもっと先端的なTypeScriptを選択しました。

local環境を整える

私の環境はMac、Big Surにバージョンアップしたばかりの出来立てほやほやMac。 なので、hombrewを使う。

homebrewでdenoをインストールする。

brew install deno

ない人はVS Codeも必要なので入れる。 そんな人はこのブログ見てないだろうけど。 Visual Studio Code – コード エディター | Microsoft Azure

VS Codeの環境を整える

この記事を書く時点ではVS CodeにはdenoのExtensionが存在しているのでそれを入れる。 marketplace.visualstudio.com

ない人はESLintとPretterのExtensionぐらいは入れておいた方が書きやすいと思う。

setting.json(denoの拡張の設定)を書く

.vscodeフォルダを作って、 以下の人のを参考にこんな感じに書きました。

{
    "deno.enable": true,
    "deno.import_map": "import_map.json",
    "[typescript]": {
        "editor.defaultFormatter": "denoland.vscode-deno",
        "editor.formatOnSave": true
    }
}

【VSCode】おすすめ拡張機能 - Deno - 開発覚書はてな版

Heroku 関連の設定

Githubリポジトリ作ってHeroku Appに結びつける

このあたりはTrailheadにTrailがあるので、それをやって勉強するといいと思います。

trailhead.salesforce.com

Heroku Appにbuild packを設定する

これ以降は以下のQiitaの記事がすごく役立った。 というか、これを見たから始めたまでもある。

qiita.com

GUIでもビルドパックは設定でき、それは先のTrailheadやってれば何となく分かると思うのでお好みでお試しください。

Procfile を書く

フォルダのトップにファイルは置く。 最終的にはこんな感じになりました。

web: deno run --allow-net=:${PORT} --allow-read --allow-env --import-map=import_map.json --unstable --log-level=${LOG_LEVEL} server.ts

${}で書かれているのは環境変数。 PORTは既定で存在するので、追加したのはLOG_LEVELだけ。 Denoはオプションで指定しないと環境変数読めなかったりなのでOFFなのでONにしたりしてます。 あと、今回の使い方だとstableのままではErrorになるのでunstableにしてます。本番システムではまだまだ使えないですね。

参考サイトだとcacheを使うようにしているのだけど、deno-expressの読み込みでErrorになるので外しました。 バージョン固定しているからキャッシュで十分なんだけどやむ無しかと。

runtime.txt を書く

これもフォルダのトップにファイルは置く。 Denoのバージョンを固定化する。ローカルにインストールされたDenoのバージョンを参考に指定した。

v1.6.0

deno-express を使う

import_map.json を書く

毎回毎回importするのにURL書くのがだるいのでマニュアルにもある機能を使う。

https://deno.land/manual@v1.6.1/linking_to_external_code/import_maps

これもフォルダのトップにファイルは置く。

{
  "imports": {
    "flags/": "https://deno.land/std@0.81.0/flags/",
    "http/": "https://deno.land/std@0.81.0/http/",
    "ws": "https://deno.land/std@0.81.0/ws/",
    "log/": "https://deno.land/x/deno_structured_logging@0.4.2/",
    "express/": "https://raw.githubusercontent.com/NMathar/deno-express/master/",
    "dotenv/": "https://deno.land/x/dotenv@v2.0.0/",
    "testing/": "https://deno.land/std@0.81.0/testing/"
  }
}

この記事時点であればexpressとdotenvだけで十分な気がする。 まだ作り途中なので後々使うものも含めて書いてます。

publicフォルダにindex.htmlを置く

見た目で動いた!って分かる方が嬉しさ100倍なので、適当なHTMLの静的ファイルを置いておきます。

フォルダのトップにpublicフォルダを新たに作成して、以下のHTMLでも置いておいてください。

<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>Deno Backend Test</title>
  </head>
  <body>
    <h1>静的コンテンツ配信テスト</h1>
    <p>表示確認用のテストページ</p>
  </body>
</html>

server.ts を書く

お待ちかね?ですかね。DenoなのでTypeScriptで書きます。

// Copyright 2020 tkm1988 All rights reserved.
import * as expressive from "express/mod.ts";
import * as rateTickerSymbol from "./controller/rateTickerSymbolController.ts";
import * as tradeSetting from "./controller/tradeSettingController.ts";
import * as userAssets from "./controller/userAssetsController.ts";

/**
 * バックエンドの使用ポートの定数(ローカルで環境変数がなければ8080を設定する)
 */
const PORT: string = Deno.env.get("PORT") ?? "8080";
/**
 * 割り当てホスト(ローカルでアクセスする場合は環境変数で127.0.0.1を指定する事)
 */
const HOST: string = Deno.env.get("HOST") ?? "0.0.0.0";
/**
 * deno-express
 */
const app: expressive.App = new expressive.App();

// # deno-expressの基本設定
// ## ログ設定
app.use(expressive.simpleLog());
// ## 静的ファイルのエンドポイント定義
app.use(expressive.static_("./public"));
// ## ボディのパーサ設定
app.use(expressive.bodyParser.json());

// ## ルーティング定義
// ### ユーザ固有の情報のCRUDのAPIに関するルーティング
// 1. ユーザのBTCの総資産を取得する
app.get("/api/user/assets", userAssets.getAllBtcAssets);

// ### ユーザ固有の情報のCRUDのAPIに関するルーティング
// 1. サポートしているティッカーシンボルのレートを取得する
app.get("/api/rate/{ticker_symbol}", rateTickerSymbol.getRateWithTickerSymbol);

// ### ユーザ固有の情報のCRUDのAPIに関するルーティング
// 1. 高値、低値、それぞれの利確指値を設定する
app.post("/api/trade/setting", tradeSetting.setTradingParams);

// # サーバ起動処理
const server = await app.listen(Number(PORT), HOST);
console.log("アプリケーション起動: ポート番号 = " + server.port);

元になるのはdeno-expressのGithubのexampleです。

github.com

この中で大切なのは以下の箇所。

/**
 * バックエンドの使用ポートの定数(ローカルで環境変数がなければ8080を設定する)
 */
const PORT: string = Deno.env.get("PORT") ?? "8080";
/**
 * 割り当てホスト(ローカルでアクセスする場合は環境変数で127.0.0.1を指定する事)
 */
const HOST: string = Deno.env.get("HOST") ?? "0.0.0.0";
...
// # サーバ起動処理
const server = await app.listen(Number(PORT), HOST);

HerokuでDeno + deno-expressのアプリを起動させるには以下の2つの事実を押さえておく必要がある。

  1. アプリケーションに動的にポートは割り当てられるので環境変数からポート番号は取得する
  2. deno-expressではlisten関数でhostを0.0.0.0で指定しないとローカルホストを指定して正常起動しない

1についてはよく記事になってるんだけど、2は案外記事がなかなか見つからない。 途方に暮れてたところで以下のQiitaの記事を見つけ、試したら起動した。 他言語だからって無下にしてはいけないですね。

LINE Bot + Python + Heroku で「Error R10 (Boot timeout) 」エラーが出たときの解消方法 - Qiita

MVCで言うControllerにあたるところを書く

個人的に、server.tsに処理をController部分だけだとしても処理を書きたくないので外出しした。

// Copyright 2020 tkm1988 All rights reserved.
import * as expressive from "express/mod.ts";

/**
 * ユーザのBTCの総資産を取得するコントローラ関数
 * 
 * @param req HTTPリクエスト
 * @param res HTTPレスポンス
 */
async function getRateWithTickerSymbol(
  req: expressive.Request,
  res: expressive.Response,
): Promise<void> {
  // dummy
  await res.json([{ data: req.params.ticker_symbol }]);
}

export { getRateWithTickerSymbol };

TSDocを使うとJava Docっぽくコメント書けるのでおすすめです。 BTCとか書いているのは私の趣味の問題なのでお気になさらず。 importを使っている(requireではない)ので、無名関数をExportできないので名前付きです。

参考サイトはこちら。

zebevogue.hatenadiary.org

さいごに

いかがでしたか?😂

以下に該当する人は是非本記事を参考にお試しください。

  • TypeScriptが流行っているからやりたい
  • どうせならトランスパイル不要なDenoで書いてみたい
    • と言うかTypeScriptの設定とかnpmの設定とか面倒臭いからDenoにしたい
  • express.jsはnode.jsで使ったことあるから同じ感じに書きたい

参考文献をいくつか載せてますが、この他にTypeScriptのコーディング規約読んだり、Denoのマニュアル読んだり、JS PrimerやMDNでJSの構文とか調べながら書いています。 つまり、変なところがあるかもしれません。ご容赦願います。 あと、開発早いから数ヶ月もしたら時代遅れな記事なっているかもなので、それもご容赦ください。