🦕

deno serve コマンドを使ったAPIサーバー開発

2024/12/02に公開

この記事は jig.jp Advent Calender 2024 の2日目の記事です。


はじめまして。入社1年目のすずともです。
ついに今年(2024年10月9日)、Deno 2 がリリースされました🎉

https://deno.com/blog/v2.0

今回紹介するのは、Deno 2 の新機能…ではないんですが、Deno v1.43 で追加されたちょっと便利な deno serve サブコマンドについて紹介します!

deno serve について

deno serve とは、Deno CLI サブコマンドの1種であり、サーバーを起動するコマンドです。

似たようなものに Deno.serve() API もありますが、簡単に言うと、この API をサブコマンドとして使えるようにしたものになります。

試しに、リクエストが来たら Hello, world と返す簡単な API サーバを作ってみましょう。

次のコードを用意して、

main.ts
export default {
  fetch(_request: Request) {
    return new Response("Hello, world");
  },
};

次のように deno serve サブコマンドにファイル名を渡して実行します

Terminal
deno serve main.ts

すると、http://localhost:8000 でサーバーが起動します!
ブラウザ等でアクセスすると 「Hello, world」 と表示されるはずです。

ちなみに、同じことを `Deno.serve()` を使って書こうとすると…
main.ts
Deno.serve((_request) => {
  return new Response("Hello, world");
});
Terminal
deno run --allow-net main.ts

となります。

この段階では、Deno.serve() API と比べて記述量も増えており、何がいいんだろう🤔 となるかと思います。

ここからは、簡易的なルーターを作って deno serve の特徴を見ていきます。

ルーターの作成

ルートの定義

下記の配列で示したルート定義が動くようにルーターを作成していきましょう。

interface Route {
  path: string;
  fn: (req: Request, param: any) => Response;
}

const routes: Route[] = [
  {
    path: "/",
    fn: () => {
      return new Response("Hello, calc 'x * x' server!");
    },
  },
  {
    path: "/square/:num",
    fn: (_, param) => {
      const num = Number(param.num); // param にはパスパラメータが入るようにする
      if (isNaN(num)) return new Response(undefined, { status: 400 });
      else return new Response(`${num * num}`);
    },
  },
  {
    path: "*",
    fn: () => {
      return new Response(undefined, { status: 404 });
    },
  },
];

動作としては、

  • ルートへのアクセスで「Hello, calc 'x * x' server!」と返す
  • /square/:num へのアクセスで num の2乗を返す
    • /square/2 だったらレスポンスは「4」となります
  • それ以外のパスへのアクセスは 404 にする

となれば OK とします!

ルーティングを行う

ルート定義が出来たので、次は、ルーティング部分を作ります。
パスパラメータを含んだ /square/:num なんて定義があるので、正規表現を… と一瞬考えてしまいますが、そんな必要はありません!

Web APIs にある URL Pattern API [1] というものを用いれば、パスパラメータを含むパスの検証が簡単に出来ます。
URL Pattern API の使い方は簡単で、以下のようにコンストラクタにパスを指定し、exec 関数でチェックするだけです。

const pattern = new URLPattern({ pathname: "/square/:num" });
const match = pattern.exec("https://example.com/square/2");
if (match) console.log(match.pathname.groups);
// Output: { num: "2" }

返り値(match)が null ならばマッチ失敗ということになります。
マッチするかどうかは、URLPattern: test() 関数でもチェックできますが、exec を使うことでパスパラメータも一緒に取ることができるため、今回は exec の方を使用しています。

URLPattern を使用することで、ルーティング部分は以下のように書けます。

export default {
  fetch(req: Request) {
    for (const route of routes) {
      const pattern = new URLPattern({ pathname: route.path });
      const match = pattern.exec(req.url);
      if (match) {
        return route.fn(req, match.pathname.groups);
      }
    }
  },
};

fetch 関数の定義を除くと)たった7行でルーティングが出来てしまいました。
正規表現もなく、直感的でわかりやすいコードだと思います。

実行

ここまで作成してきたコードを使って API サーバーを起動してみます。

コード全文はこちら
main.ts
interface Route {
  path: string;
  fn: (req: Request, param: any) => Response;
}

const routes: Route[] = [
  {
    path: "/",
    fn: () => {
      return new Response("Hello, calc 'x * x' server!");
    },
  },
  {
    path: "/square/:num",
    fn: (_, param) => {
      const num = Number(param.num); // param にはパスパラメータが入るようにする
      if (isNaN(num)) return new Response(undefined, { status: 400 });
      else return new Response(`${num * num}`);
    },
  },
  {
    path: "*",
    fn: () => {
      return new Response(undefined, { status: 404 });
    },
  },
];

export default {
  fetch(req: Request) {
    for (const route of routes) {
      const pattern = new URLPattern({ pathname: route.path });
      const match = pattern.exec(req.url);
      if (match) {
        return route.fn(req, match.pathname.groups);
      }
    }
  },
};

冒頭にも示したように

Terminal
$ deno serve main.ts
deno serve: Listening on http://localhost:8000/

とするだけで簡単に起動できます!🎉

ブラウザで各URLにアクセスして確認してみてください

deno serve の特徴

ここまで、ルート定義を元に処理を振り分ける API サーバーを書いてきました。
ここまでで何か気づいたことはないでしょうか?

記述したコードをよく見てみると…なんと、Deno 依存のコード(簡単に言うと Deno. で始まるもの)がないのです!

deno serve コマンドと Web の機能だけでサーバーを記述することにより、以下の利点があります。

実行ランタイムが Deno に縛られない

JS の実行環境は Deno 以外にも、Node.jsBun などがあります。それらのランタイムで実行することも理論上可能です。

特に Bun は TS のまま実行可能なため、下記のように書くことで Bun で動くサーバを立てることが可能です。

bun-server.ts
import Server from "./main.ts";

const server = Bun.serve({
  fetch: Server.fetch,
});
console.log(`Listening on http://localhost:${server.port}`);

// 実行コマンド
// bun run bun-server.ts

その他にも、Cloudflare Workers は fetch 関数でリクエストを受信できるという同じ形式を採用しているため、TS から JS への変換は必要になるものの、コード内部には手を付けずにデプロイが可能です。[2]

テストを行いやすい

APIサーバーのテストをするには、フレームワークが用意しているテストの仕組みを使用したり、実際にサーバーを起動してアクセスする などの方法が考えられます。

しかし、今回作成したコードでは、エクスポートされた fetch 関数を呼び出すだけで簡単にテストが出来ます。

例えば…

test.ts
import { assertEquals } from "jsr:@std/assert";

import Server from "./main.ts";

Deno.test("値の2乗が返ってくる", async () => {
  const req = new Request("https://example.com/square/2");
  const res = Server.fetch(req);
  const body = await res?.text();
  assertEquals(body, "4");
});
Terminal
$ deno test test.ts

とするとテストが出来ます。

パーミッション指定が要らない

Deno ではコードを安全に実行するための仕組みが用意されており、ファイル読み書きやネットワークを使用するためにはコマンド実行時にパーミッションを明示的に指定する必要があります。

例えば、ファイルを読み込みたい場合には

Terminal
$ deno run --allow-read main.ts

という感じになります。

APIサーバを立てる際、普通であればリクエストの送受信にネットワークを使用するので、--allow-net フラグが必要になります。

しかし、deno serve コマンドを使うときにはこのフラグは要らなくなります。
コマンドも短くなりますし、無駄にパーミッションを許可しなくて良くなるためセキュリティ面でも安心できます。

もちろん、内部でファイルの読み書き、ネットワークを使用するときには別途パーミッション指定することで実行することが可能です。

`--allow-net` フラグにホストを指定して厳密にすることも出来ますが…

ローカルホストで起動するときには --allow-net=localhost
外部サーバーで起動するときには --allow-net=example.com
などと使い分けなくてはいけならず、少し面倒です。

ホスト名やポート番号をプログラム内で管理する必要がない

ホスト名、ポート番号、証明書ファイルなどは、本来、コード側では意識する必要のないものです。
それらの指定を deno serve コマンドが担ってくれるおかげで、コード内にはポート番号を指定したり、環境変数を読み込んでいる箇所はありません。

コマンドの引数に --port--host などのオプションが設定可能です。

Terminal
$ deno serve --port=3000 --host=127.0.0.1 main.ts
deno serve: Listening on http://127.0.0.1:3000/

ポート番号が環境変数で与えられる場合でも --port=$PORT とするだけです。
オプションの詳細は deno serve コマンドのドキュメント をご覧ください。

最後に

今回の記事では、簡易的なルーターを作ってみましたが、本格的な API サーバーとして動かすには、ネストしたルートへの対応や、ミドルウェア機能など、まだまだ機能が足りません。

やはりその辺りを自分で作るのは難しく、結局は既存のフレームワークの方が便利だったりするのですが、"ランタイムに依存しないフレームワーク" という夢のようなフレームワークがでてきて、Node.js でも Bun でも Cloudflare Workers でも Lambda@Edge etc... でも動作可能になると面白いなと個人的には思っています。

脚注
  1. URL Pattern API はまだ実験段階の API であり、ブラウザでは Chromium 系しか対応していませんが、ランタイムの Deno, Node.js, Bun では既に使用可能です。 ↩︎

  2. 今回の例ではモジュールのインポートをしていないため上記の例でも動作しますが、ランタイムによりモジュール解決法が違うので動かない場合もあります。その場合も Webpack などのモジュールバンドラーを使用することで実行が可能です。 ↩︎

jig.jp Engineers' Blog

Discussion