Deno Deep Dive - Denoでのアプリケーション実装Tips / Node.jsとの比較

## 概要

本記事ではNode.jsに代わるサーバサイドJavaScriptランタイムであるDenoについて、実際のアプリケーション実装で得られた知見をご紹介します。

まずはDenoの概要について説明し、アプリケーションを実装する上でのDenoの便利な使い方やAPI、各種ツールについて解説していきます。最後に筆者がDenoによる開発を経てNode.jsによる開発と比較した際のDenoの優れている点/不足している点を挙げていきます。

## Denoとは

DenoはNodejsの開発者であるRyan DahlがNode.jsの反省を活かして作り出したJavaScriptランタイムです。

https://deno.land/

https://www.youtube.com/watch?v=M3BM9TB-8yA

特徴としては以下のものがあります。

- TypeScriptが**out of box**、つまり設定なしで実行できる
- ES Modules対応で依存モジュールはURLによってインポートする
- 権限管理が厳密
- フォーマッタやテストランナなどの便利機能がランタイムに付属

まずTypeScriptについて、Denoは**TypeScriptで書いたスクリプトを直接実行することができます**。例えばちょっとした作業スクリプトを書く場合にも、または本格的なアプリケーションを書く場合にも設定なしですぐにTypeScriptが利用できます。スクリプトを実行する際に事前にトランスパイルやビルドを実行する必要はありません。内部で実行されるDenoのコンパイラはRustで実装されており、高速でトランスパイル/ビルドされます。

DenoはES Modulesを活用したモジュールシステムを採用しており、外部モジュールはURLを使ってインポートしていきます。Node.jsでは外部モジュールはプロジェクトフォルダ内のnode_modulesにダウンロードしていました。Denoは実行時にURLから読み込めばいいのでプロジェクト内に巨大なnode_modulesフォルダを作る必要がありません。もちろん実行のたびにダウンロードすると時間がかかってしまうのでしまうので、ランタイムがキャッシュしてくれます。

Denoではネットワークやファイルシステムに対するアクセス制御を行うことができます。明示的に指定しない限りそれらへのアクセスはできない仕様です。Node.jsと比べてセキュアなランタイムと言えます。

またDenoのランタイムにフォーマッタやテストランナが付属しており、外部ライブラリをインストールする必要なく`deno`コマンドによって実行可能です。この辺りの具体的な使い方についても後述します。

## Denoでのアプリケーション実装Tips

### 実装事例

弊社がリリースしたDockpit liteというツールのバックエンドAPIをDenoで実装しました。

https://chrome.google.com/webstore/detail/dockpit-lite/mjbgnnaedaplnoahfejcifokefbhmalg

DenoのHTTP APIミドルウェアであるoakを活用し、認証や別APIからのデータの取得/加工などの機能を実装しています。

https://github.com/oakserver/oak

以下ではこのAPIサーバの実装で利用したDenoの機能やユーティリティ、外部ライブラリの使い方を説明していきます。

### 付属機能/API

#### アプリケーションの実行

DenoではJavaScript/TypeScriptを`deno run`コマンドで実行します。TypeScriptで書いたスクリプトを直接実行できます。Node.jsではtsconfig.jsonを書いたり、webpackなどモジュールバンドラの設定をしたりしなければTypeScriptを実行することはできませんでした。

```bash
deno run index.ts
```

`deno run`コマンドにオプションを指定しない限りネットワーク/ファイルシステム/環境変数などにアクセスすることはできません。以下のようにフラグを付与して実行することでアクセス許可を与えることができます。またファイルシステムの読み書きなどについてはアクセスを許可する範囲を限定することが可能です。

```bash
deno run --allow-net --allow-read=. --allow-write=./log index.ts
```

#### 依存パッケージのインポート

Denoでは依存パッケージはURLからインポートします。

```ts
import { decode, encode } from 'https://deno.land/std@0.120.0/encoding/base64url.ts';
import { Application } from 'https://deno.land/x/oak@v9.0.1/mod.ts';
```

Deno標準のライブラリとよく利用されるサードパーティのライブラリはdeno.landにアップロードされています。

[std@0.126.0 | Deno](https://deno.land/std)

[Third Party Modules | Deno](https://deno.land/x)

例えばAPIミドルウェアのoakライブラリをインポートする場合は`https://deno.land/x/oak@v[version]/mod.ts`を指定します。ここに置かれているソースコードはブラウザ上でも閲覧できるようになっています。またES Modulesなのでソースコードのモジュールファイル(`mod.ts`)から直接必要なものをインポートできます。つまり、ブラウザ上でライブラリのソースコードを閲覧して必要なものを直接URLによってインポートできるということです。このURLインポートとES Modulesの組み合わせによってライブラリを活用した開発体験が良いのもDenoの特徴です。

#### 内部モジュールのインポート

内部モジュールはNode.jsと同様にプロジェクトフォルダ内の相対パスでインポートできます。ただしDenoではインポートするモジュールの拡張子が必要な点に注意が必要です。

```ts
import { ApiError } from './utils/Error.ts';
```

またRyan DahlがNode.jsの反省点に挙げていたように、Denoにはフォルダ内のindex.jsをそのフォルダ名でインポートする機能はありません。ファイルシステムの構造通りにインポートします。

#### インポートマップ

さて、以上で述べたようなURLでのインポートや相対パスでのインポートを毎回書くのは面倒です。そこでDenoにはインポートマップという機能があります。

[Import maps | Manual | Deno](https://deno.land/manual/linking_to_external_code/import_maps)


この機能によって、絶対パスでのモジュールのインポートや外部ライブラリインポートURLの省略(エイリアスの作成)ができます。以下のようなJSONにエイリアスとその内容を記述していきます。

```json:importMap.json
{
    "imports": {
        "@/": "./",
        "oak": "https://deno.land/x/oak@v9.0.1/mod.ts"
    }
}
```

このインポートマップのパスを実行時にオプションで指定します。

```bash
deno run --importmap=importMap.json index.ts
```

このように設定することによって、以下のようにインポートできるようになります。

```ts
import { Application } from 'oak';
import { ApiError } from '@/utils/Error.ts';
```

インポートマップの機能は記述の省略以外にも依存ライブラリのバージョンの管理に役立ちます。アプリケーション全体でライブラリのバージョンを変更したい場合はインポートマップの記述のみを修正すれば良いからです。インポートマップを利用せずにそれぞれのモジュールでバージョンを記述しているとその全てを修正する必要があります。

#### watchモード

こうして様々なライブラリやモジュールを利用しながら開発を進めていきます。開発中のアプリケーションを更新するためには`deno run`を再実行する必要がありますが、これは非常に面倒です。Denoにはwatchモードがあり、ソースの変更を検知して自動でアプリケーションを再起動してくれます。

[Command line interface | Manual | Deno](https://deno.land/manual@v1.17.1/getting_started/command_line_interface#watch-mode)

```bash
deno run --watch index.ts
```

#### 環境変数

完成したアプリケーションをデプロイするに際して、対象の環境に応じて環境変数を変更して動作を変えることがよくあります。Denoでは標準APIの`Deno.env`によって環境変数を利用することができます。

```ts
> Deno.env.set('ENVIRONMENT', 'development');
> Deno.env.get('ENVIRONMENT');
"development"
```

また.envファイルに記述した環境変数を読み込むなら`dotenv`ライブラリが利用できます。

```env:.env.development
ENVIRONMENT="development"
```

```ts
import { config } from "https://deno.land/x/dotenv@v3.2.0/mod.ts";
config({path: './.env.development', export: true});
Deno.env.get('ENVIRONMENT');
```

Denoで環境変数利用する場合、実行時に`--allow-env`フラグを付ける必要があります。逆に言えばこのフラグを付与しなければ意図しない環境変数の読み取り/変更を防ぐことができます。

```bash
deno run --allow-env index.ts
```

#### トップレベルawait

Node.js 14.8以降で使えるトップレベルawaitがDenoでもデフォルトで利用できます。トップレベルawaitとは、スクリプト実行時に非同期関数外でawaitできる機能です。例えば以下のようなコードを実行することができます。

```ts
const data = await fetch('https://localhost:8000/user/get', {
    method: 'POST',
    body: JSON.stringify({ userid: 1 }),
});
console.log(data);
console.log(await data.json());
```

この機能により、非同期処理やPromiseオブジェクトの扱いが簡単になります。

#### フォーマッタ

DenoにはJavaScriptでいうprettierにあたるフォーマッタが標準搭載されています。

[Formatter | Manual | Deno](https://deno.land/manual/tools/formatter)

`deno fmt`コマンドで実行してソースコードの整形が行えます。細かいフォーマッタの設定は以下のようにdeno.jsonに記述します。

```json:deno.json
{
	"fmt": {
		"options": {
			"useTabs": false,
			"singleQuote": true,
            "indentWidth": 4,
            "lineWidth":120
		}
	}
}
```

この設定ファイルを利用してフォーマットする際は実行時のオプションで設定ファイルへのパスを指定します。

```bash
deno fmt --config ./deno.json
```

#### テストランナ

Denoにはテストランナが標準搭載されています。Node.jsでいうJESTといった外部ライブラリのインストールは不要です。

[Testing | Manual | Deno](https://deno.land/manual/testing)

`deno test`コマンドでテストコードを実行できます。自動でプロジェクトフォルダ内のjs/tsファイルがチェックされ、テストコードが見つかると実行されます。テストコードの記述には`Deno.test`APIを利用します。

```ts:test.ts
import { assertEquals } from "https://deno.land/std@0.126.0/testing/asserts.ts";

Deno.test("test 1", () => {
    const x = 1 + 2;
    assertEquals(x, 3);
});

Deno.test("test 2", () => {
    const x = 1 + 2;
    assertEquals(x, 5);
});
```


```bash
$ deno test
running 2 tests from file:///.../test.ts
test test 1 ... ok (10ms)
test test 2 ... FAILED (13ms)

failures:

test 2
AssertionError: Values are not equal:


    [Diff] Actual / Expected


-   3
+   5

...
```

またネットワークやファイルにアクセスするテストを実行する場合には`deno run`と同様に各種オプションをつけて`deno test`を実行する必要があります。

### 外部ツール

#### スクリプトランナ

以上で見たように、細かい設定を行なった上でDenoを実行するためには`deno run`や`deno test`に様々なオプションを付ける必要があります。そうした場合に以下のように`deno run`コマンドが長くなってしまいます。

```bash
deno run --watch --importmap=importMap.json --allow-net --allow-env --allow-read=. --allow-write=./log index.ts
```


これを解決してくれるのがスクリプトランナーのvelociraptorです。

https://github.com/jurassiscripts/velociraptor

次のようなyamlを定義しておくとコマンド実行時のフラグ、インポートマップ、環境変数ファイルを指定することができます。複数のコマンドを使い分ける場合にもそれぞれにオプションを記述できて便利です。

```yaml:velociraptor.yaml
allow:
  - net
  - env

scripts:
  start-dev:
    cmd: deno run index.ts
    envFile: .env.development
    watch: true
    imap: importMap.json
    allow:
      - read=.
      - write=./log
  start:
    cmd: deno run index.ts
    envFile: .env.production
    watch: true
    imap: importMap.json
    allow:
      - read=.
      - write=/var/log
```

velociraptorをインストールした上で、以下のコマンドで設定ファイルに指定した各コマンドを実行できます。Node.jsでいうpackage.jsonへのコマンドの記述のイメージです。

```bash
vr start-dev
vr start
```

velociraptorは実際には設定ファイルと.envファイルを読み取って以下のようなコマンドを生成しています。こちらを利用する場合は環境変数はファイルから読み取ってあらかじめシェルの環境変数にセットされているので、dotenvライブラリは不要になります。Deno上で.evnファイルの環境変数を利用する場合は直接`Deno.env.get()`するだけです。

```bash
ENVIRONMENT="development" \
deno run \
  --watch --importmap=importMap.json --allow-net --allow-env --allow-read=. --allow-write=./log \
  index.ts "$@"
```

サーバ環境でvelociraptorをインストールしたくない場合は`vr export`で設定した各コマンドをシェルスクリプトに展開しておくことができます。そのシェルスクリプトをサーバにデプロイして実行するだけでローカル環境と同じ動作になります。

```bash
vr export start-dev
vr export start
```

#### エディタ

開発するエディタとしてVSCodeを想定します。Denoで実行するTypeScriptを普通に開いても、VSCodeはNode.js用の構文解析をしてしまいます。Deno用の構文解析をさせるにはVSCodeにvscode-deno拡張機能をインストールする必要があります。

https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno


ワークスペースでDenoの拡張機能を有効にするためには.vscode/settings.jsonに以下のように記述します。

```json:.vscode/settings.json
{
    "deno.enable": true,
    "deno.lint": true,
    "deno.unstable": true,
    "deno.config": "deno.json",
    "deno.suggest.imports.hosts": {
        "https://deno.land": true
    },
    "deno.importMap": "./importMap.json",
    "editor.formatOnSave": true,
    "[typescript]": {
        "editor.defaultFormatter": "denoland.vscode-deno"
    }
}
```

ここにインポートマップの指定もしておくと、インポートパスをエイリアスで指定していてもエディタ上でエラー表示になりません。

また`editor.formatOnSave`を有効にしてファイルを自動フォーマットさせる際に先述のDeno付属用のフォーマッタの設定を利用するには`deno.config`の項目で設定ファイル(`deno.json`)を指定しておきます。こうすると毎回`deno fmt`を実行しなくてもファイルの保存と同時にフォーマットすることができます。

## Node.jsとの比較

Denoは以下の点でNode.jsより優れていると考えます。

- TypeScriptを利用した際の開発体験の向上
- デプロイの簡略化
- 権限管理の厳密化

Node.jsと比較した際に、やはりTypeScriptで書いたスクリプトを直接実行できる点によって開発体験が向上しているのを感じます。トランスパイラやモジュールバンドラといった依存要素が大幅に減っていることで、アプリケーションの実装を始めるまでの時間的/心理的ハードルが低くなっています。

またTypeScriptを直接実行可能なことでDenoは「スクリプト言語としてのTypeScript」を発明したといってもいいかもしれません。業務上簡単なスクリプトを書いて仕事を効率化する際にもTypeScriptを利用することができます。ざっくり言うとDeno+TypeScriptはPythonに近い使用感があります。この点においてもDenoは非常に便利だと感じています。

また実装した成果物をサーバにデプロイする際に、Node.jsではソースコードを配置した上で依存ライブラリのインストール、ビルド、アプリケーションの起動と3プロセスが必要です。一方でDenoではソースコードを配置してそれを直接実行できます。ライブラリのインストールとビルドの2プロセスが省略されます。

もちろん上記の二つのポイントについてはTypeScriptやECMAScriptを利用しない際はNode.jsとDenoでそれほど差異はありません。しかしながら現代において複雑なアプリケーションをJavaScriptを実装する際にそれらを利用しないことはほとんど考えられないでしょう。

最後に権限管理について、Denoではネットワークやファイル、環境変数へのアクセスに明示的な許可が必要です。不必要な権限を与えることがないのでNode.jsで実装した場合よりセキュアになります。筆者は幸いにもNode.jsアプリケーションに対するハッキング被害にあったことはありませんが、やはり権限の管理は厳しいに越したことはないでしょう。

一方で以下の点でNode.jsに及んでいないと考えます。

- サードパーティライブラリのエコシステム
- フロントエンド開発での利用

Node.jsの歴史は長く、膨大な数のサードパーティライブラリが存在します。一方のDenoにもある程度サードパーティライブラリは揃っているものの、総数では及ぶべくもありません。ただしサードパーティライブラリがないなら自分で実装すればいい、という発想もあり得ます。筆者もCookieの署名やJWTの検証をDeno標準のエンコードライブラリを用いて実装したりしました。

またReactなどフロントエンド開発フレームワークを用いる場合は依然としてNode.jsを利用する必要があります。DenoでもAlephといったフロントエンド開発のフレームワークが開発されていますが、未だベータ版です。

https://github.com/alephjs/aleph.js

これらの点については今後のエコシステムの発達によってNode.jsに追いつくこともできるかもしれません。

## おわりに

本記事ではアプリケーションを実装する上でのDenoの便利な機能や外部ツールを解説しました。また現時点でのDenoとNode.jsの優劣についても述べていきました。

個人的にはJS/TSでのサーバサイドのアプリケーションの実装や日々の業務効率化スクリプト実装においてDenoを積極的に利用していきたいと考えています。本記事で関心を持たれた方は是非利用してみてください。