見出し画像

【Node.js】Expressでroutesの一覧を成形してログ出力してみた

はじめに

こんにちは、SHIFTの開発部門に所属している Katayama です。

REST API を Node.js Express で実装していた際、プロジェクトの規模がでかくなった時に、今どこまで実装したのか?が分からなくなる事もあるのではと思っていた。そこで、Express のroutes(ルーティング)を一覧で出力できないだろうかと思い、少し調査してみた結果、案外簡単に実現できそうだという事が分かったのでやってみた。

最終的にどのようなものになるかだが、以下のようにログに Express のルーティング一覧が出力されるものを実現する。

※本記事の内容は、Node.js で import・export(ES6 の構文)を使えるように webpack × Babel の設定をやってみたにあるような Webpack によるビルド(トランスパイル)を行うプロジェクトを前提としている。

実際にやってみる

app._router.stack について

まず、How to get all registered routes in Express?に書かれている通り、Express には"app._router.stack"でアクセスできる Layer オブジェクトの配列がある。これには middleware が階層になって登録されている。

具体的にどんな middleware が登録されているか?を見てみると、以下の例ではcompressionjsonParserなどが登録されている事が分かるだろう("..."の部分は省略の意)。

そして、その中に"name: 'router'"であるものが見つけられるが、これはrouterで作成された Router middleware になる。ここの Layer オブジェクトの"handle.stack"の配列にルーティングに関する情報が格納されている。また、app._router.stack[].route に Route オブジェクトを持つ Layer オブジェクトもルーティングに関する情報が格納されている。

[
  ...
  Layer {
    handle: [Function: compression],
    name: 'compression',
    params: undefined,
    path: undefined,
    keys: [],
    regexp: /^\/?(?=\/|$)/i { fast_star: false, fast_slash: true },
    route: undefined
  },
  Layer {
    handle: [Function: serveStatic],
    name: 'serveStatic',
    params: undefined,
    path: undefined,
    keys: [],
    regexp: /^\/?(?=\/|$)/i { fast_star: false, fast_slash: true },
    route: undefined
  },
  Layer {
    handle: [Function: jsonParser],
    name: 'jsonParser',
    params: undefined,
    path: undefined,
    keys: [],
    regexp: /^\/?(?=\/|$)/i { fast_star: false, fast_slash: true },
    route: undefined
  },
  ...
  Layer {
    handle: [Function: router] {
      params: {},
      _params: [],
      caseSensitive: undefined,
      mergeParams: undefined,
      strict: undefined,
      stack: [Array]
    },
    name: 'router',
    params: undefined,
    path: undefined,
    keys: [],
    regexp: /^\/api\/v1\/?(?=\/|$)/i { fast_star: false, fast_slash: false },
    route: undefined
  },
  Layer {
    handle: [Function: bound dispatch],
    name: 'bound dispatch',
    params: undefined,
    path: undefined,
    keys: [ [Object] ],
    regexp: /^(.*)\/?$/i { fast_star: true, fast_slash: false },
    route: Route { path: '*', stack: [Array], methods: [Object] }
  }
]

ルーティングの情報を取り出し、ログに出力する

上記で見たように、ルーティングの情報を取り出してログに出力させるには、"app._router.stack"から app._router.stack[].handle.stack を持つかつ、app._router.stack[].name が"router"であるものを取り出し、その配列の中身を console.log すればいい。
また、app._router.stack[].route に Route オブジェクトを持つ Layer オブジェクトの"route"にもルーティング情報が含まれるので、それを取り出し console.log すればいい。

という事で実装としては以下のようにしてみた。

// src/lib/console-express-routing.js
import { strict as assert } from "assert";
import { toUpper } from "lodash";

export default (options = {}) => {
  assert.ok(options.app, "app must be required");

  const {
    app: {
      _router: { stack: layers },
    },
  } = options;
  const routingList = {};

  const routerStacks = layers.filter(
    (layer) => layer.handle.stack && layer.name === "router"
  );
  const routeLayers = layers.filter((layer) => layer.route);

  routerStacks.forEach((routerStack) => {
    const basePath = routerStack.regexp
      .toString()
      .replaceAll("\\", "")
      .replace("/^", "")
      .replace("/?(?=/|$)/i", "");

    routerStack.handle.stack.forEach((stack) => {
      const {
        route: { path, methods },
      } = stack;

      if (routingList[`${basePath}${path}`]) {
        routingList[`${basePath}${path}`].push(
          toUpper(Object.keys(methods).shift())
        );
        return;
      }
      routingList[`${basePath}${path}`] = [
        toUpper(Object.keys(methods).shift()),
      ];
    });
  });

  routeLayers.forEach((layer) => {
    const {
      route: { path, methods },
    } = layer;

    if (routingList[path]) {
      routingList[path].push(toUpper(Object.keys(methods).shift()));
      return;
    }
    routingList[path] = [toUpper(Object.keys(methods).shift())];
  });

  console.log(routingList);
};

上記の実装でいくつか補足をする。

  • layers.filter((layer) => layer.handle.stack && layer.name === 'router'))
    ここでルーターのミドルウェアとして登録されている Layer オブジェクトを絞り込んでいる

  • routerStack.regexp.toString().replaceAll(...).replace(...).replace(...)
    routerStack.regexp.toString()の結果は、"/^/api/v1/?(?=/|$)/i"のようにルーティングのパスに合致する正規表現になっているのだが、そこからルーティングのパスの部分だけを抜き出すための処理をここで行っている。
    つまり、"app.use('/api/v1', router)"のように実装した場合の、"/api/v1"の部分を抽出している。
    これはベースパスとしてルーティング一覧をログに出力する際に使っている。

  • toUpper(Object.keys(methods).shift())
    以下で出てくるが、methods は"methods: { patch: true }"のようなオブジェクトになっており、これが HTTP メソッドの種類を表している。ここでは methods のオブジェクトから HTTP メソッドの部分を大文字に変換して取り出す、という事をしている。

上記のコードで、実際にログにルーティング一覧が出力できるか?検証してみると、ルーティング一覧を出力できている事が確認できる。

// src/index.js
import express, { Router } from "express";
// 省略
import consoleExpressRouting from "./lib/console-express-routing";
// 省略

app.use("/api/v1", router);

app.get("*", (req, res) => {
  res.sendFile(appRoot.resolve("static/index.html"));
});

router.post("/user", async (req, res) => {
  // 省略
});
router.get("/users", async (req, res) => {
  // 省略
});
router.get("/user/:id", async (req, res) => {
  // 省略
});
router.patch("/user/:id", async (req, res) => {
  // 省略
});
router.get("/user/id/:userId", async (req, res) => {
  // 省略
});
router.get("/", (req, res) => {
  // 省略
});

// routing一覧を出力
consoleExpressRouting({ app });

// 省略
app.listen(3000, () => console.log("listening on port 3000!"));
study@localhost:~/workspace/node-express (main *)
$ node dist/index.js
{
  '/api/v1/user': [ 'POST' ],
  '/api/v1/users': [ 'GET' ],
  '/api/v1/user/:id': [ 'GET', 'PATCH' ],
  '/api/v1/user/id/:userId': [ 'GET' ],
  '/api/v1/': [ 'GET' ],
  '*': [ 'GET' ]
}
listening on port 3000!

※ちなみに、以下のように"routerStack.handle.stack"の配列の各要素を取り出すと、以下のようになっている。

const routerStacks = layers.filter(
  (layer) => layer.handle.stack && layer.name === "router"
);
routerStacks.forEach((routerStack) => {
  routerStack.handle.stack.forEach((stack) => {
    console.log(stack);
  });
});
...
Route {
  path: '/user/:id',
  stack: [
    Layer {
      handle: [AsyncFunction (anonymous)],
      name: '<anonymous>',
      params: undefined,
      path: undefined,
      keys: [],
      regexp: /^\/?$/i,
      method: 'patch'
    }
  ],
  methods: { patch: true }
}
...

vue-cli-plugin-express のように装飾してみる

上記の実装でも Express のルーティングを一覧で出力する、というのは達成できているが、折角なのでvue-cli-plugin-expressのように、きれいにルーティングを一覧化する、というのをやってみたいと思う。

と言っても、実装自体は vue-cli-plugin-express のlogSuccessLaunch.jsにあるものとほぼ同じになる。

// src/lib/console-express-routing.js
// 省略
import Table from "cli-table3";
import chalk from "chalk";

const methodsColors = {
  OPTIONS: "grey",
  GET: "green",
  POST: "blue",
  PUT: "yellow",
  PATCH: "yellow",
  DELETE: "red",
};
const visibleMethods = Object.keys(methodsColors);

const prepareMethods = (methods) =>
  methods
    .filter((method) => visibleMethods.includes(method))
    .sort(
      (first, second) =>
        visibleMethods.indexOf(first) - visibleMethods.indexOf(second)
    )
    .map((method) => chalk[methodsColors[method] || "default"](method))
    .join(", ");

const createApiRoutesTable = (routingList) => {
  const table = new Table({
    chars: {
      top: "",
      "top-mid": "",
      "top-left": "",
      "top-right": "",
      bottom: "",
      "bottom-mid": "",
      "bottom-left": "",
      "bottom-right": "",
      left: "",
      "left-mid": "",
      mid: "",
      "mid-mid": "",
      right: "",
      "right-mid": "",
      middle: "  ",
    },
    style: { "padding-left": 0, "padding-right": 0, compact: true },
  });

  Object.keys(routingList).forEach((key) =>
    table.push([`    - ${key}:`, prepareMethods(routingList[key])])
  );

  return table;
};

export default (options = {}) => {
  // 省略
  console.log("  🔀 Api routes found:");
  console.log(createApiRoutesTable(routingList).toString());
};

上記の実装に関していくつか補足をする。

  • new Table({...})
    cli-table3というライブラリを利用している。リストっぽくログに出力させるために、テーブル構造のボーダー線を消してルーティング一覧を表示するようにしている。

  • prepareMethods = (methods) => ...
    ここでは"methodsColors"に定義してる HTTP メソッドに含まれるか?のチェックと、GET、POST、...の順に並ぶように sort 関数で順番を入れ替え、chalkというライブラリでログに出力される際のテキストカラーを設定し、最後に HTTP メソッドをカンマ区切りの文字列に変換する、という事を行っている。

上記のコードで、装飾された形でログにルーティング一覧が出力できるか?検証してみると、きれいにルーティング一覧を出力させられている事が確認できる。

まとめとして

今回は Express に登録されている routes(ルーティング)一覧をログに出力させる、という事をやってみた。開発をしている時、REST API の開発で実装をしていく毎に、API が可視化されて情報整理される事で、開発者体験の向上にもつながっているのではないかと思った。

《この公式ブロガーの記事一覧》


執筆者プロフィール:Katayama Yuta
認証認可(SHIFTアカウント)や課金決済のプラットフォーム開発に従事。リードエンジニア。
経歴としては、SaaS ERPパッケージベンダーにて開発を2年経験。
SHIFTでは、GUIテストの自動化やUnitテストの実装などテスト関係の案件に従事したり、DevOpsの一環でCICD導入支援をする案件にも従事。その後現在のプラットフォーム開発に参画(1年半)。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/