Node.jsの後悔から生まれた新しい実行環境・Deno入門 〜簡単なアプリケーション作成ハンズオン付き〜

こんにちは!小田島です。ウェブ業界に来る前は手品業界で働いていました。最近は外出自粛で手品をやる機会がないので家でひたすら練習しています。

前回の記事「いまさら聞けないNode.js」は、「わかりやすい」「いい記事」「背景の説明が丁寧」といった好意的な反応が多くて安心しました。

Denoについては後日記事を書きます」と前回宣言したので、今回はDenoについての入門記事を書きます。よろしくおねがいします!

対象者

今回は、こんな人が対象です。

  • Denoって何?
  • Node.jsとどう違うの?
  • 全然触ったことないけど何か簡単で応用が効くものを作ってみたい

前回と違い、ハンズオンも含まれています。ぜひ読みながら実際に動かしてみてください。

Denoとは?

前回同様に超ざっくりと説明すると、JavaScriptとTypeScriptの動作環境です。作者はNode.jsと同じライアン・ダールです。DenoはNodeのアナグラムでしょうか。ライアン・ダールがNode.jsを設計したときの後悔をもとに再設計したものと言え、バージョン1.0.0は2020年5月13日にリリースされました。つい最近ですね。

Node.jsと同じくシングルプロセス・シングルスレッド・非同期I/Oに基づいて設計されていますので、正しく書けばC10K問題は発生しません。C10K問題についての詳細や非同期I/O、気を付けるべき点などについては前回の記事をご覧ください。また、Node.jsはJavaScriptだけしか実行できませんが、DenoはTypeScriptのコードもそのまま実行できます

ちなみにDenoは「での」ではなく「でぃーの」と読む…と勝手に思っていたのですが、ライアン・ダール自身は「での」と読んでいますマスコットキャラクター(名称不明)がかわいいです。

Deno

Denoのマスコットキャラクター(名称不明)

もうちょっと詳しく

細かい説明はいいからとにかくハンズオンやらせろ!というせっかちさんは読み飛ばして「インストール方法」まで進んでも問題ありません。そうでない方はもう少しお付き合いください。先に言っておきますがちょっと長いです

JavaScriptエンジンには、Node.jsと同じV8を採用しています。JavaScriptエンジンだけではなくTypeScriptコンパイラも内包しているので、tscやts-nodeなどを使わずにTypeScriptのソースコードをそのままDeno上で実行できます。バージョン1.0.0の時点でV8エンジンのバージョンは8.4.300で、これは本記事執筆時点で最新のNode.jsより新しいです(現時点で最新のNode.jsは14.4.0、V8エンジンは8.1.307.31)。

また、TypeScriptのバージョンは3.9で、これも本記事執筆時点で最新です。ECMAScriptの最新規格をほぼ網羅している&TypeScriptコンパイラも内蔵しているということで、Babelやtscなどで変換せずにそのままコードを実行できるというのは嬉しいですね。例えばECMAScriptの比較的新しい機能であるBigIntトップレベルawaitなどを使えます。余談ですが、BigIntに関しては以前Qiitaに記事を書いたので、よろしければご覧ください。

Node.jsとの違い

「じゃあ、Denoは新しいV8エンジンとTypeScriptを使えるだけで基本的にはNode.jsと変わらないの?

違います。

どちらも同じエンジンをベースにしているので文法は互換性がありますが、実際には単一ファイルだけで完結するような単純なプログラムでない限り、まずそのままでは動きません。最初に書いた通り、DenoはNode.jsの後悔をもとに再設計したものなので、Node.jsの設計上の問題点が解消されています。その代償として、既存のNode.js向けのほとんどのコードはそのままでは実行できなくなってしまいました。

Denoでは大きな変更点が4つあります。詳細はライアン・ダール自身による「Node.jsにおける10の後悔」をご覧ください。

Promiseとasync/awaitを非同期処理の標準として採用

標準ライブラリーが刷新され、非同期関数はコールバックではなくPromiseが返るようになりました。これにより、コールバック地獄に悩まされずにわかりやすく書けるようになりました。逆にいうと、標準ライブラリーもNode.jsと互換性がありません

プログラムが必要な権限を実行時に指定

例えばファイルへの書き込みが必要な場合は実行時に--allow-writeを、ネットワークアクセスが必要な場合は--allow-netを実行時オプションとして指定する必要があります。これにより、万一セキュリティーホールを突かれた場合でも被害を軽減できます。例えばファイルアクセスが必要ないアプリケーションであれば、セキュリティーホールを突かれてもファイルへの被害はありません。

モジュールはrequire()を廃止し、ES Modulesに統一

Node.jsでは、モジュール形式として長らくCommonJS形式のrequire()/module.exportsが使われていました。途中からES Modulesがサポートされましたが使うには実行時オプションが必要で、最近になってようやくデフォルトで使えるようになりました。Denoではこれを一本化し、モジュールはES Modules形式のみサポートしています。もちろん実行時オプションは不要です。

インポートするときは、エントリーポイントとなるファイルを直接指定

個人的にはこれが一番大きな変更だと思っています。

まず、Node.jsではモジュールパスの解決方法がカオスです。例えばimport "foo"という文があった場合、

  1. 標準ライブラリーとしてのfoo
  2. npm installでインストールしたfooパッケージ
  3. NODE_PATH内にあるfoo.js
  4. NODE_PATH内にあるfoo/index.js

この辺りを検索します。多分他にも検索している場所はあると思いますが複雑すぎて追い切れていません。この複雑さゆえにimport "foo"だけでは最終的にどれがインポートされるのかすぐにはわからず、コードリーディングしづらかったり、意図しないものを取り込んでしまうバグを引き起こしたりすることもありました。そして今度は拡張子どうする議論が出てきて.mjsとか.cjsが提案されたりもうわけがわかりません。この辺りの流れについては去年Qiitaに記事を書きましたので興味があればご覧ください。

このカオスな状況を解決するため、Denoではモジュールの扱いを大胆かつシンプルにしました。覚えておくのは以下の3点だけです。

  1. 指定可能なモジュールのパスは、絶対パスまたは元ファイルからの相対パス(./foo.ts)のみ
  2. URL形式(例: https://deno.land/std@0.55.0/http/server.ts)の絶対パスも指定可
  3. 拡張子やindex.jsは省略不可

これにより、どのファイルをインポートしているのかが明確になりました。またパッケージという概念がなくなり、いかなる場合でもインポートしたいファイル名をピンポイントで指定することを要求されます。標準ライブラリーすらもURLで指定する必要があります。

逆に、この変更のために既存のNPMパッケージ等はほぼ使えなくなりました。上で「単一ファイルだけで完結するような単純なプログラムでない限りまずそのままでは動きません」と書いたのはこれが理由です。

ここまでのまとめ

  • DenoはJavaScriptとTypeScriptの実行環境
  • 読み方は「での」らしい
  • バージョン1.0.0時点のV8エンジンはNode.jsよりも新しく、TypeScriptも最新のバージョンが入っている
  • 標準ライブラリーは非同期処理にPromiseを採用
  • 実行時に必要な権限のオプション指定が必要
  • モジュール形式はES Modulesのみ
  • インポート対象のモジュールは、絶対パスまたは相対パスで拡張子含めてピンポイントで指定
  • マスコットキャラがかわいい

これでDenoについての基本的な説明は終わりです。次からはいよいよ実際に動かしてみましょう。

インストール方法

お待たせしました。ようやくハンズオンの始まりです。今回使うソースコードはすべてGitHubに置いています。

エディターはVisual Studio Codeをオススメします。上記のGitHubリポジトリーをgit cloneしたら、Visual Studio Codeで「ファイル」→「ワークスペースを開く」からproject.code-workspaceを選択してください。そのままだとimport文のところでエラーが発生するので、Deno拡張機能をインストールしておきましょう。上記手順でワークスペースを開くと、自動的に拡張機能をインストールするか尋ねられます。

それでは最初にDenoをインストールしてみましょう。何も難しくありません。公式ページに従ってコマンドを1行打ち込むだけです。

Windowsの場合:

iwr https://deno.land/x/install/install.ps1 -useb | iex

macOS/Linuxの場合:

curl -fsSL https://deno.land/x/install/install.sh | sh

実行すると、ホームディレクトリーの下に.denoというディレクトリーが作られます。中を見てみると、binの中にバイナリーファイルが1つだけ。これがDenoの一にして全です。

とりあえず動かしてみる

インストールは無事終わりましたか?ではさっそく動かしてみましょう。

何も考えずに以下の内容を無心でコンソールに入力してください。

deno run https://deno.land/std/examples/welcome.ts

「Welcome to Deno 🦕」というメッセージが表示されたら成功です。

deno run https://deno.land/std/examples/welcome.ts の実行結果

え、全然プログラミングしてる実感ないんですけど。

デスヨネ。では、次はちょっとプログラミングっぽいことをやってみましょう。次の内容をdeno-example.tsという名前で保存してください。

import {serve} from "https://deno.land/std@0.55.0/http/server.ts";
const s = serve({port: 8000});
console.log("http://localhost:8000/");
for await (const req of s) {
    req.respond({body: "Hell Word\n"});
}

さっきと同じように実行してみましょう。

deno run deno-example.ts

実行できましたか?できませんね。

こんなメッセージが表示されていると思います。

権限エラーで実行できない!

上で書いた「プログラムが必要な権限を実行時に指定」というのがこのことです。このプログラムはネットワークアクセスが必要なので、以下のように実行してみましょう。

deno run --allow-net deno-example.ts

今度はプログラムが終わりませんが、それで問題ありません。そのままブラウザーでhttp://localhost:8000/にアクセスしてみてください。

deno-example.tsを実行後にブラウザーからアクセス

地獄の言葉(Hell Word)が表示されました。おめでとうございます。どのパスにアクセスしても(例えば http://localhost:8000/a など)地獄の言葉しか出力しませんが、れっきとしたウェブアプリケーションです。コンソールでCtrl+Cを押すとアプリケーションが終了します。

参考までに、Node.jsで同様の機能を作る場合は以下のようなソースコードになります。

const http = require("http");
console.log("http://localhost:8000/");
http.createServer((request, response) => {
    response.end("Hell Word\n");
}).listen(8000);

今回は非常に単純な内容なのでコード量はほとんど変わりませんが、細かい関数の使い方はさておきDenoのコードは以下のような特徴があるのがわかると思います。

  • require()ではなく、ES Modulesのimport文を使っている
  • モジュールファイルのURLを指定している
  • トップレベルでawait文(for await of構文)を使っている

ウェブアプリケーションフレームワークを使ってみる

本格的なウェブアプリケーションを作る場合は、フレームワークを使った方が便利です。今回はServestというフレームワークを使ってみましょう。先ほど作ったアプリケーションをServestを使って書き直すと、以下のようになります。

import {createApp} from "https://servestjs.org/@v1.1.0/mod.ts";
const app = createApp();
console.log("http://localhost:8000/");
app.get("/", async (req) => {
    await req.respond({
        status: 200,
        body: "Hell Word",
    });
});
app.listen({port: 8000});

import文にファイルパスを指定するだけで使えます。package.jsonnpm installも必要ありません。簡単ですね。

先程とあまり代わり映えがしないように見えますが、ウェブアプリケーションとしては2つの大きな違いがあります。

  • 先程のコードはどんなパスにアクセスしても地獄の言葉を返していましたが、今度はルートパスにだけ反応するようになりました。それ以外のパス(http://localhost:8000/aなど)にアクセスすると404エラーが返ります。
  • 先程のコードはすべてのリクエストメソッドに対して地獄の言葉を返していましたが、今度はGETメソッドのみ受け付けるようになりました。POSTやPUTに対しては何も反応しません。

このように、フレームワークを使うと「どのパス」に対する「どんなリクエストメソッド」を処理するかを簡単に記述できるようになります。このあたりを自前で書くと結構面倒くさいので、フレームワークを使って開発効率を上げましょう。

ちなみに、Denoでは自作モジュールの入口はこのServestのように慣習的にmod.tsにすることが多いようです。

Web APIっぽいものを作ってみる

では次に、Web APIのようなものを作ってみましょう。本格的なアプリケーションではデータはDBから取得しますが、今回は簡単に実行できるように内部にデータを持っています。

import {createApp} from "https://servestjs.org/@v1.1.0/mod.ts";

interface User {
    id: number
    name: string
}
const users: User[] = [
    {id: 1, name: "Pablo Picasso"},
    {id: 2, name: "Pablo Diego Picasso"},
    {id: 3, name: "Pablo Diego José Picasso"},
    {id: 4, name: "Pablo Diego José Francisco Picasso"},
    {id: 5, name: "Pablo Diego José Francisco de Paula Picasso"},
    {id: 6, name: "Pablo Diego José Francisco de Paula Juan Picasso"},
    {id: 7, name: "Pablo Diego José Francisco de Paula Juan Nepomuceno Picasso"},
    {id: 8, name: "Pablo Diego José Francisco de Paula Juan Nepomuceno Cipriano Picasso"},
    {id: 9, name: "Pablo Diego José Francisco de Paula Juan Nepomuceno Cipriano de la Santísima Picasso"},
    {id: 10, name: "Pablo Diego José Francisco de Paula Juan Nepomuceno Cipriano de la Santísima Trinidad Picasso"},
    {id: 11, name: "Pablo Diego José Francisco de Paula Juan Nepomuceno Cipriano de la Santísima Trinidad Ruiz Picasso"},
];

const app = createApp();
console.log("http://localhost:8000/");
app.get("/users", async (req) => {
    await req.respond({
        status: 200,
        headers: new Headers({
            "content-type": "application/json",
        }),
        body: JSON.stringify(users),
    });
});
app.get(new RegExp("^/users/(\\d+)"), async (req) => {
    const [_, id] = req.match;
    const filtered = users.filter(user => user.id === Number(id))
    if (filtered.length === 0) {
        // 見つからなかった
        return;
    }

    await req.respond({
        status: 200,
        headers: new Headers({
            "content-type": "application/json",
        }),
        body: JSON.stringify(filtered[0]),
    });
});
app.listen({port: 8000});

このコードを適当なファイル名で保存し(例えばwebapi-example.ts)、先程と同じように実行します。

deno run --allow-net webapi-example.ts

実行したらhttp://localhost:8000/usersにアクセスしてください。users変数に入っている値が一覧で出力されるはずです。また、http://localhost:8000/users/1にアクセスすると最初の値("Pablo Picasso")が、http://localhost:8000/users/100では404エラーが表示されます。

今回のポイントはルーティングとJSON出力です。/users/1のように動的なパスも正規表現で対応でき、"content-type"を指定すればJSONを出力できます。

パラメーターを処理してみる

実際にウェブサービスを作る場合は、入力パラメーターの処理が必要です。今度は/usersに以下のパラメーターを渡せるようにしてみましょう。

  • q
    • 検索語句
    • 文字列
    • 省略時は空文字列
  •  limit
    • 取得するデータ数の上限
    • 整数(小数部は切り捨て)
    • 指定できる値は1以上20以下(範囲外の値が指定されたらエラー)
    • 省略時は10
  • offset
    • 取得開始位置(先頭は0)
    • 整数(小数部があればエラー)
    • 指定できる値は0以上(負の値が指定されたら0とみなす)
    • 省略時は0

ウェブアプリケーションを作った経験がある方ならわかると思いますが、パラメーターを適切に処理するのってかなり面倒なんですよ。

数値が必要な場合、数値として解釈できる文字列が指定された場合は?扱える範囲外の値が指定された場合は? "1e+10" のような表記は受け入れる?パラメーターが省略されたらどうする?

文字列が必要な場合、受け付けるパターンは?長さの上限・下限は?上限をオーバーした場合は切り詰める?パラメーターが省略されたらどうする?

他にもboolean型や配列、オブジェクトなどの型があります。さらに配列やオブジェクトのネストまで考えだしたらもうキリがありません。

面倒だなぁ…

どこかにいいライブラリーがないかなぁ…

 

なんと偶然にもこんなところにいい感じのライブラリーが!

 

すみませんちょっと変なテンションになってました。今から紹介するのは、そんな煩わしいパラメーター処理を一手に引き受けてくれる拙作の神ライブラリー・value-schemaです。

使い方は簡単。以下のコードを実行してみてください。

import vs from "https://deno.land/x/value_schema/mod.ts";

const schemaObject = {
    q: vs.string({                      // 文字列型
        ifEmptyString: "",                  // 空文字列を許可(デフォルトでは空文字列はエラー)
        ifUndefined: "",                    // 省略時は空文字列
    }),
    limit: vs.number({                  // 数値型
        integer: vs.NUMBER.INTEGER.FLOOR,   // 整数(小数部は切り捨て)
        minValue: 1,                        // 1以上(違反したらエラー)
        maxValue: 20,                       // 20以下(違反したらエラー)
        ifUndefined: 10,                    // 省略時は10
    }),
    offset: vs.number({                 // 数値型
        integer: vs.NUMBER.INTEGER.YES,     // 整数(小数部があればエラー)
        minValue: {
            value: 0,                           // 最小値は0
            adjusts: true,                      // 違反したら範囲に収まるように調整(負の数を0にする)
        },
        ifUndefined: 0,                     // 省略時は0
    }),
};

console.log(vs.applySchemaObject(schemaObject, {
    limit: "1.8",
    offset: -1,
}));

次のように出力されます。

{ q: "", limit: 1, offset: 0 }

文字列だったlimitが数値化されて小数部が切り捨てられ、0より小さいoffsetが0に調整され、存在しなかったqにデフォルト値の空文字列が入っているのがわかりますね。

ね、簡単でしょう?

ロジックを一切書かずに「あるべき状態」を定義するだけでバリデーションや変換を行ってくれます。他にも「全角数字の文字列も数値型に変換したい」「数値型でなければ、たとえ数値化できる場合でもエラーにしたい」「RFCに準拠したURLやメールアドレスかどうかチェックしたい」「カンマ区切りの文字列を配列化したい」などあらゆる要望に応えられる高機能ライブラリーです。もちろんTypeScriptに対応しているのでコード補完もバッチリです。詳しくはREADMEをどうぞ。

というわけで先程のAPIをvalue-schemaを使って書き直すと以下のようになります。

import {createApp} from "https://servestjs.org/@v1.1.0/mod.ts";
import vs from "https://deno.land/x/value_schema/mod.ts";

const schemaObject = {
    q: vs.string({                      // 文字列型
        ifEmptyString: "",                  // 空文字列を許可(デフォルトでは空文字列はエラー)
        ifUndefined: "",                    // 省略時は空文字列
    }),
    limit: vs.number({                  // 数値型
        integer: vs.NUMBER.INTEGER.FLOOR,   // 整数(小数部は切り捨て)
        minValue: 1,                        // 1以上(違反したらエラー)
        maxValue: 20,                       // 20以下(違反したらエラー)
        ifUndefined: 10,                    // 省略時は10
    }),
    offset: vs.number({                 // 数値型
        integer: vs.NUMBER.INTEGER.YES,     // 整数(小数部があればエラー)
        minValue: {
            value: 0,                           // 最小値は0
            adjusts: true,                      // 違反したら範囲に収まるように調整(負の数を0にする)
        },
        ifUndefined: 0,                     // 省略時は0
    }),
};

interface User {
    id: number
    name: string
}
const users: User[] = [
    {id: 1, name: "Pablo Picasso"},
    {id: 2, name: "Pablo Diego Picasso"},
    {id: 3, name: "Pablo Diego José Picasso"},
    {id: 4, name: "Pablo Diego José Francisco Picasso"},
    {id: 5, name: "Pablo Diego José Francisco de Paula Picasso"},
    {id: 6, name: "Pablo Diego José Francisco de Paula Juan Picasso"},
    {id: 7, name: "Pablo Diego José Francisco de Paula Juan Nepomuceno Picasso"},
    {id: 8, name: "Pablo Diego José Francisco de Paula Juan Nepomuceno Cipriano Picasso"},
    {id: 9, name: "Pablo Diego José Francisco de Paula Juan Nepomuceno Cipriano de la Santísima Picasso"},
    {id: 10, name: "Pablo Diego José Francisco de Paula Juan Nepomuceno Cipriano de la Santísima Trinidad Picasso"},
    {id: 11, name: "Pablo Diego José Francisco de Paula Juan Nepomuceno Cipriano de la Santísima Trinidad Ruiz Picasso"},
];

const app = createApp();
console.log("http://localhost:8000/");
app.get("/users", async (req) => {
    const query: Record<string, string> = {};
    for (const [k, v] of req.query.entries()) {
        query[k] = v;
    }
    const normalizedQuery = vs.applySchemaObject(schemaObject, query);
    const filteredUsers = users
        .filter(user => user.name.indexOf(normalizedQuery.q) != -1)
        .slice(normalizedQuery.offset, normalizedQuery.offset + normalizedQuery.limit);

    await req.respond({
        status: 200,
        headers: new Headers({
            "content-type": "application/json",
        }),
        body: JSON.stringify(filteredUsers),
    });
});
app.listen({port: 8000});

http://localhost:8000/users?limit=5http://localhost:8000/users?limit=10&offset=5http://localhost:8000/users?q=Trinidadなどにアクセスしてみてください。パラメーターに応じて出力内容が変わるのが確認できるでしょう。

今回はパラメーターのエラーチェックをしていないのでhttp://localhost:8000/users?limit=xのようなURLだとエラーが表示されますが、実際のアプリケーションではきちんとエラーハンドリングを行いましょう。value-schemaではどのプロパティーがどの制限に引っかかっているかという詳細な情報を取得できるので、丁寧なエラーメッセージを返せます。

余談ですが、ライブラリーの正式名称はvalue-schemaですが、deno.landにはハイフンが含まれているモジュールを登録できなかったのでURLはvalue_schema(アンダースコア)です。ややこしくてすみません。

全部合体させる

今回の総仕上げです。今まで作ったコードを合体させてみましょう。

import {createApp} from "https://servestjs.org/@v1.1.0/mod.ts";
import vs from "https://deno.land/x/value_schema/mod.ts";

const schemaObject = {
    q: vs.string({                      // 文字列型
        ifEmptyString: "",                  // 空文字列を許可(デフォルトでは空文字列はエラー)
        ifUndefined: "",                    // 省略時は空文字列
    }),
    limit: vs.number({                  // 数値型
        integer: vs.NUMBER.INTEGER.FLOOR,   // 整数(小数部は切り捨て)
        minValue: 1,                        // 1以上(違反したらエラー)
        maxValue: 20,                       // 20以下(違反したらエラー)
        ifUndefined: 10,                    // 省略時は10
    }),
    offset: vs.number({                 // 数値型
        integer: vs.NUMBER.INTEGER.YES,     // 整数(小数部があればエラー)
        minValue: {
            value: 0,                           // 最小値は0
            adjusts: true,                      // 違反したら範囲に収まるように調整(負の数を0にする)
        },
        ifUndefined: 0,                     // 省略時は0
    }),
};

interface User {
    id: number
    name: string
}
const users: User[] = [
    {id: 1, name: "Pablo Picasso"},
    {id: 2, name: "Pablo Diego Picasso"},
    {id: 3, name: "Pablo Diego José Picasso"},
    {id: 4, name: "Pablo Diego José Francisco Picasso"},
    {id: 5, name: "Pablo Diego José Francisco de Paula Picasso"},
    {id: 6, name: "Pablo Diego José Francisco de Paula Juan Picasso"},
    {id: 7, name: "Pablo Diego José Francisco de Paula Juan Nepomuceno Picasso"},
    {id: 8, name: "Pablo Diego José Francisco de Paula Juan Nepomuceno Cipriano Picasso"},
    {id: 9, name: "Pablo Diego José Francisco de Paula Juan Nepomuceno Cipriano de la Santísima Picasso"},
    {id: 10, name: "Pablo Diego José Francisco de Paula Juan Nepomuceno Cipriano de la Santísima Trinidad Picasso"},
    {id: 11, name: "Pablo Diego José Francisco de Paula Juan Nepomuceno Cipriano de la Santísima Trinidad Ruiz Picasso"},
];

const app = createApp();
console.log("http://localhost:8000/");
app.get("/", async (req) => {
    await req.respond({
        status: 200,
        body: "Hell Word",
    });
});
app.get("/users", async (req) => {
    const query: Record<string, string> = {};
    for (const [k, v] of req.query.entries()) {
        query[k] = v;
    }
    const normalizedQuery = vs.applySchemaObject(schemaObject, query);
    const filteredUsers = users
        .filter(user => user.name.indexOf(normalizedQuery.q) != -1)
        .slice(normalizedQuery.offset, normalizedQuery.offset + normalizedQuery.limit);

    await req.respond({
        status: 200,
        headers: new Headers({
            "content-type": "application/json",
        }),
        body: JSON.stringify(filteredUsers),
    });
});
app.get(new RegExp("^/users/(\\d+)"), async (req) => {
    const [_, id] = req.match;
    const filtered = users.filter(user => user.id === Number(id))
    if (filtered.length === 0) {
        // 見つからなかった
        return;
    }

    await req.respond({
        status: 200,
        headers: new Headers({
            "content-type": "application/json",
        }),
        body: JSON.stringify(filtered[0]),
    });
});
app.listen({port: 8000});

http://localhost:8000/http://localhost:8000/users?limit=10&offset=5http://localhost:8000/users/5など、これまで作ったエンドポイントが全て使えるようになります。

ハンズオンは以上です。お疲れさまでした!

Denoに対する個人的な不満

Denoをしばらく使っていると、確かにNode.jsのイケてない点がかなり改善されているというのは実感できます。

一方で、個人的にはモジュール周りの仕様に戸惑っています。これは単なる慣れの問題かというとそうでもなく、設計思想に関わる話のように思います。

  • 外部モジュールのバージョン指定の方法に一貫性がない
    • モジュールのバージョン情報もURLに含める必要があるが、指定方法が統一されておらずホスティングサービスによってまちまち
    • https://deno.land/std@0.55.0/http/server.ts / https://servestjs.org/@v1.1.0/mod.tsなど
    • そもそも必ずバージョン指定できるとは限らず、ホスティングサービスによっては常に最新のものがダウンロードされる可能性もある
  • 依存モジュールのバージョンを上げるときはソースコードのあちこちを修正する必要がある
    • インポートマップはアプリケーションのための機能であって、アプリケーションが依存するライブラリーからは使えない
    • 同じようなことを思っている人が多いのか、有志が色々なモジュール管理ツールを作っている様子

他にも、出たばかりでライブラリーがまだまだ少ないという点もありますが、これは仕方ないですね。どんな言語でも最初はそんなもんなので、みんなで盛り上げましょう。設計思想上仕方ないとはいえ、NPMの膨大な資産をごっそり切り捨てたのはもったいないと思っています。

あと、これは日本語限定の話ですがググラビリティーがよくないです。例えば "deno mac" と検索すると「macでの〜」を含む記事がヒットするので、現状では "deno" をクォーテーションで囲む必要があります。

メジャーになっていくにつれて検索エンジン側も学習してくれると期待しましょう。

まとめ

背景からハンズオンまで詰め込みまくったのでかなり分量が多くなってしまいました。冒頭に書いた本記事の対象者を再掲します。

  • Denoって何?
  • Node.jsとどう違うの?
  • 全然触ったことないけど何か簡単で応用が効くものを作ってみたい

いかがでしょうか。Denoに関する疑問が解決したり、簡単なウェブアプリケーションを作ったことで今後の創作意欲が刺激されていれば幸いです。

パラメーター処理が楽になるvalue-schemaもぜひ使ってください。NPM版もあります。

Twitterもやってます。今回の記事に関する感想やvalue-schemaに関する質問、手品のお話等お待ちしてます!