
Lambda 上の Express でカスタムミドルウェアを作成してリクエストログを出力してみた
製造ビジネステクノロジー部の小林です。
最近、 Lambda 上で動く Express アプリケーションのリファクタリングの際に、リクエストログ出力処理を Express のミドルウェアとして切り出す機会がありました。
書きながら気づいたのですが、「なぜそう書くか」がいまいち言語化できなかったので、公式ドキュメントを読み直して実際にカスタムミドルウェアを作るまでの流れ整理してみました。
前提
- Express 5 を前提にしています。Express 4 では req.body の初期値など一部挙動が異なります。
- Express は Lambda 上で動かします。
3 行まとめ
- Express のカスタムミドルウェアは、(req, res, next) の 3 引数を受け取る関数 1 本を書く。
- 処理を書いたら、最後に next() を呼んで次のミドルウェアに渡す。
- 書いたミドルウェアは app.use() で登録する。登録順がそのまま実行順。
作ったもの
最終的に書いたコードを先に貼ります。HTTP リクエストの内容を CloudWatch Logs に流すための、シンプルなログミドルウェアです。
構成は下記のとおりです。
EXPRESS-MIDDLEWARE/
├── cdk/
├── node_modules/
├── src/
│ ├── middleware/
│ │ └── logEventMiddleware.ts
│ ├── app.ts
│ └── server.ts
├── package.json
├── pnpm-lock.yaml
└── tsconfig.json
import { Request, Response, NextFunction } from "express";
const logEventMiddleware = (
req: Request,
_res: Response,
next: NextFunction,
): void => {
console.log(
"Request:",
JSON.stringify(
{
method: req.method,
path: req.path,
params: req.params,
query: req.query,
headers: req.headers,
body: req.body,
},
null,
2,
),
);
next();
};
export default logEventMiddleware;
import express from "express";
import serverlessExpress from "@codegenie/serverless-express";
import logEventMiddleware from "./middleware/logEventMiddleware";
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(logEventMiddleware);
app.post("/echo", (req, res) => {
res.json({ received: req.body });
});
export default app;
export const handler = serverlessExpress({ app });
書き始める前に知っておくこと
Express の公式ドキュメントでは、ミドルウェア関数を次のように定義しています。
ミドルウェア関数とは、アプリケーションのリクエスト・レスポンスサイクルにおいて、リクエストオブジェクト(req)、レスポンスオブジェクト(res)、および次のミドルウェア関数にアクセスできる関数です。次のミドルウェア関数は、通常、next という名前の変数で表されます。
要するに、req・res・next の 3 つを引数に取る関数を書くということです。そして公式が強調している重要なルールがひとつあります。
現在のミドルウェア関数がリクエスト・レスポンスサイクルを終了しない場合は、next() を呼び出して次のミドルウェア関数に制御を渡す必要があります。そうしないと、リクエストは未完了のままになります。
ポイントは、「レスポンスを返して処理を終わらせる」か「next() を呼んで次のミドルウェアに渡す」かのどちらかを必ず行うということです。これを忘れると、リクエストが宙ぶらりんのまま放置されてしまいます。
この前提を押さえて実装に入っていきます!
やってみる
引数の 3 点セット
const logEventMiddleware = (
req: Request,
_res: Response,
next: NextFunction,
): void => {
| 引数 | 型 | 役割 |
|---|---|---|
req |
Request |
リクエスト情報(メソッド、パス、ヘッダー、ボディなど) |
_res |
Response |
レスポンスを返すためのオブジェクト |
next |
NextFunction |
次のミドルウェアに処理を渡す関数 |
ポイントを 2 つ。
Request/Response/NextFunctionの型はexpressパッケージから import します。_resの先頭アンダースコアは「受け取るけど使わない」を表します。今回のミドルウェアはログを出して次に流すだけでレスポンスを返さないので、_resは未使用扱いにしました。
リクエスト情報を JSON で出力
console.log(
"Request:",
JSON.stringify(
{
method: req.method,
path: req.path,
params: req.params,
query: req.query,
headers: req.headers,
body: req.body,
},
null,
2
)
);
next() で次に進む
next();
ログを出力して終わり、ではなく、next() を呼んで次のミドルウェアに処理を渡します。これを書き忘れると、リクエストが Express の中で止まったままになってしまいます。
app.use() で登録する
書いたミドルウェアを Express アプリに繋ぎ込みます。
import express from "express";
import serverlessExpress from "@codegenie/serverless-express";
import logEventMiddleware from "./middleware/logEventMiddleware";
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(logEventMiddleware);
app.post("/echo", (req, res) => {
res.json({ received: req.body });
});
export default app;
export const handler = serverlessExpress({ app });
app.use() で登録した順に、上から下へ実行されます。
ログミドルウェアは express.json() の後ろに置いています。順番が逆だと、req.body がまだパースされていない状態でログ出力することになり、ボディが undefined になります。
また、全リクエストに対して効かせたいミドルウェアは、ルーティングより前に登録する必要があります。
動作確認
上記で作成したコードを Lambda にデプロイし、curl で POST リクエストを送ってみます。
curl -X POST https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/v1/echo \
-H "Content-Type: application/json" \
-d '{"name":"shoma","msg":"hello"}'
CloudWatch のログを見ると、リクエストの headers、body などが JSON 形式でちゃんと出力されていますね。

まとめ
カスタムミドルウェアを 1 本書く流れをおさらいします。
(req, res, next)の 3 引数を受け取る関数を書く- やりたい処理を書く(今回はリクエスト情報のログ出力)
- 最後に
next()を呼んで次に渡す app.use()で Express アプリに登録する
以上です!どなたかの参考になれば幸いです。
参考





