AWS Lambda 用ミドルウェアフレームワークの作成方法を middy を題材に検証（１）

## 目次
[1.　イントロダクション](#1-イントロダクション)
[2.　ミドルウェアフレームワークの基本構造と概要](#2-ミドルウェアフレームワークの基本構造と概要)
[3.　ミドルウェアの登録](#3-ミドルウェアの登録)
[4.　パイプライン処理](#4-パイプライン処理)
[次の記事](https://qiita.com/Katsumi1972/items/d8e89d347a3f1bd11d4f)に記載
5.　エラー処理の実装
6.　機能の追加
7.　基本機能の仕上げ

## 1.イントロダクション

このドキュメントは、[`middy`](https://middy.js.org)のcoreモジュールを参考にして、実験的なミドルウェアフレームワーク「miggy」の開発チュートリアルです。<font color="red">middy/coreのソースコードを逐次的にコピーしながら、</font>ミドルウェアフレームワークの仕組みを理解していきます。ただし、middy/coreと完全に同じにすることを目指すのではなく、ミドルウェアフレームワークの基本機能を理解する程度まで作業を進めます。そのため、完成したmiggyは実用レベルではありません。チュートリアル終了後は、`middy`をご利用ください。

### このチュートリアルで得られること

- このチュートリアルを終えると、自信を持ってmiddyの利用を開始できるようになります。
- 自作のミドルウェアをmiddyに登録する方法について検討できるようになります。
- middyを使用して、プロジェクトに適したカスタムのAWS Lambda用ミドルウェアを開発することができるようになります。

### このチュートリアルでは得られないこと

- middyが提供しているミドルウェアについての説明は行いません。たとえば、middyのvalidatorやhttp-json-body-parserの設計については触れません。

### 対象読者

- APP Lambda (Node.js) 開発者
- ミドルウェアフレームワークの設計について知りたい開発者
- Decoratorパターンを知っており、実装例を知りたい開発者

### 対象外の読者

- デザインパターンにある程度慣れており、自身でモジュールなどの設計を行っている開発者は、middy/coreのソースコードを直接解読することで十分なので、このチュートリアルは不要です！

### 参考にするモジュール

- [middy](https://middy.js.org)の`v5.0`のソースコード
    - middy/coreのソースコードを段階的にコピーしながら理解を深めるチュートリアルです。

### 環境など

- node: v16.14.2
- モジュール: CommonJS
- typescript: 未使用

---

## 2. ミドルウェアフレームワークの基本構造と概要

### 2-1. Lambda ハンドラー関数を返す

ミドルウェアフレームワークは ASW Lambda のハンドラー関数を返します。

#### miggy1.js

```js:miggy1.js
const MiddlewareFramework = () => {
  const lambdaHandler = async (event, context) => {
    return "ダミーレスポンス";
  };

  return lambdaHandler;
};

module.exports.miggy = MiddlewareFramework;
```

#### index1.js

```js:index1.js
const { miggy } = require("./miggy1.js");

const handler = miggy();
exports.handler = handler;

console.log("hander: ", handler.toString());

const event = { artist: "米酢元気", song: "Lemon酢" };
const context = { isContext: true };
handler(event, context).then((result) => {
  console.log("handlerのresult: ", result);
});
```

ローカルで実行して AWS Lambda のハンドラー関数が返ってくることを確認します。

```sh
$ node index1.js
hander:  async (event, context) => {
    console.log("lambdaHandler.event:   ", event);
    console.log("lambdaHandler.context: ", context);

    return "ダミーレスポンス";

  }
lambdaHandler.event:    { artist: '米酢元気', song: 'Lemon酢' }
lambdaHandler.context:  { isContext: true }
handlerのresult:  ダミーレスポンス
```

### 2-2. メイン処理(ビジネスロジック)を実行する

メイン処理を実行する。前処理と後処理が実行されるべき箇所も確認しておきます。

#### miggy2.js

```js:miggy2.js
const defaultMainHandler = () => {};

const MiddlewareFramework = (mainHandler = defaultMainHandler) => {
  const lambdaHandler = async (event = {}, context = {}) => {
    console.log("ここで前処理を実行");

    let result = await mainHandler(event, context);

    console.log("ここで後処理を実行");

    return result;
  };

  return lambdaHandler;
};

module.exports.miggy = MiddlewareFramework;
```

ビジネスロジックの関数を作成し、miggy のパラメーターとして渡して実行します。

#### index2.js

```js:index2.js
const { miggy } = require("./miggy2.js");

const businessLogic = async (event, context) => {
  console.log("ビジネスロジック実行中");

  const result = {
    title: "<<< ビジネスロジック >>>",
    message: event.artist + "さんこんにちは！",
    event,
    context,
  };

  return result;
};

const handler = miggy(businessLogic);
exports.handler = handler;

const event = { artist: "米酢元気", song: "Lemon酢" };
const context = { isContext: true };

handler(event, context).then((result) => {
  console.log("handlerのresult: ", result);
});
```

ローカルで実行して、処理の流れが問題ないか確認します。

```sh
$ node index2.js
ここで前処理を実行
ビジネスロジック実行中
ここで後処理を実行
handlerのresult:  {
  title: '<<< ビジネスロジック >>>',
  message: '米酢元気さんこんにちは！',
  event: { artist: '米酢元気', song: 'Lemon酢' },
  context: { isContext: true }
}
```

---

## 3. ミドルウェアの登録

### 3-1.ミドルウェア登録のインタフェース定義

ミドルウェアを登録する際の関数/メソッドとして use(middleware) がよく使われます。middy と同様に miggy でも use(middleware) でのミドルウェア登録時に、登録するミドルウェアが以下のいずれの種類のものかを区別する必要があります。

- 前処理
- 後処理
- エラー処理

それぞれのミドルウェアは以下のフォーマットで登録されます。これは実際のミドルウェア関数を JSON でラップした形式と言えます。

#### 前処理用ミドルウェアの登録

```text
{
  before: middleware関数
}
```

#### 後処理要ミドルウェアの登録

```text
{
  after: middleware関数
}
```

#### エラー処理要ミドルウェアの登録

```text
{
  onError: middleware関数
}
```

#### 異なる種類のミドルウェアを同時に登録することも可能です

```text
{
  before:   middleware関数,
  after:    middleware関数,
  onError:  middleware関数
}
```

### 3-2. use(middleware)の実装

#### miggy3.js

```js:miggy3.js
const defaultMainHandler = () => {};

const MiddlewareFramework = (mainHandler = defaultMainHandler) => {
  const beforeMiddlewares = [];
  const afterMiddlewares = [];
  const onErrorMiddlewares = [];

  const lambdaHandler = async (event = {}, context = {}) => {
    // 前処理の実行
    for (const nextMiddleware of beforeMiddlewares) {
      await nextMiddleware();
    }

    // メイン処理
    let result = await mainHandler(event, context);

    // 後処理の実行
    for (const nextMiddleware of afterMiddlewares) {
      await nextMiddleware();
    }

    console.log("エラー処理は未対応だが、登録しているミドルウェアを順次実行");
    for (const nextMiddleware of onErrorMiddlewares) {
      await nextMiddleware();
    }

    return result;
  };

  lambdaHandler.use = (middlewares) => {
    if (!Array.isArray(middlewares)) {
      middlewares = [middlewares];
    }
    for (const middleware of middlewares) {
      const { before, after, onError } = middleware;

      if (!before && !after && !onError) {
        throw new Error(
          '"before", "after", "onError" キーのいずれかでミドルウェア関数を登録して下さい'
        );
      }

      if (before) lambdaHandler.before(before);
      if (after) lambdaHandler.after(after);
      if (onError) lambdaHandler.onError(onError);
    }
    return lambdaHandler;
  };

  lambdaHandler.before = (beforeMiddleware) => {
    // FIFO
    beforeMiddlewares.push(beforeMiddleware);
    return lambdaHandler;
  };
  lambdaHandler.after = (afterMiddleware) => {
    // FILO
    afterMiddlewares.unshift(afterMiddleware);
    return lambdaHandler;
  };
  lambdaHandler.onError = (onErrorMiddleware) => {
    // FILO
    onErrorMiddlewares.unshift(onErrorMiddleware);
    return lambdaHandler;
  };

  return lambdaHandler;
};

module.exports.miggy = MiddlewareFramework;
```

#### index3.js

```js:index3.js
const { miggy } = require("./miggy3.js");

const businessLogic = async (event, context) => {
  console.log("ビジネスロジック実行中");

  const result = {
    title: "<<< ビジネスロジック >>>",
    message: event.artist + "さんこんにちは！",
    event,
    context,
  };

  return result;
};

const handler = miggy(businessLogic)
  .use({ before: () => console.log("before 1") })
  .use({ before: () => console.log("before 2") })
  .use({ before: () => console.log("before 3") })
  .use({ after: () => console.log("after 1") })
  .use({ after: () => console.log("after 2") })
  .use({ after: () => console.log("after 3") })
  .use({ onError: () => console.log("onError 1") })
  .use({ onError: () => console.log("onError 2") });

exports.handler = handler;

const event = { artist: "米酢元気", song: "Lemon酢" };
const context = { isContext: true };

handler(event, context).then((result) => {
  console.log("handlerのresult: ", result);
});
```

#### ローカルで実行して確認します

```sh
$ node index3.js
before 1
before 2
before 3
ビジネスロジック実行中
after 3
after 2
after 1
エラー処理は未対応だが、登録しているミドルウェアを順次実行
onError 2
onError 1
handlerのresult:  {
  title: '<<< ビジネスロジック >>>',
  message: '米酢元気さんこんにちは！',
  event: { artist: '米酢元気', song: 'Lemon酢' },
  context: { isContext: true }
}
```

---

## 4. パイプライン処理

### 4-1. パイプラインの設計

ミドルウェアを繋げるためには、統一した実行インタフェースが必要となります。今回も middy の仕様をそのまま miggy に適用します。

#### ミドルウェア関数のパラメータ定義

ミドルウェア関数のパラメータは1つであり、miggy によって以下のように呼び出されます。

```js
middleware(request);
```

この時、request は以下のフォーマットになります。

```text
{
  "event":    /* Lambda実行時のevent */
  "context":  /* Lambda実行時のcontext */
  "response": /* Lambdaで返すレスポンス */
  "error":    /* 実行時のエラー */
}
```

- response や error はミドルウェアが動的に設定可能です。
- event や context の属性もミドルウェアは動的に変更可能であり、それによりミドルウェア間の連携やビジネスロジック(mainHandler)との連携が可能になります。
  - フォーマット変換前処理ミドルウェアでは、event.body にある JSON 文字列を JSON オブジェクトに変換して再設定することが可能です。
  - Logger を request.context.logger に前処理用ミドルウェアで設定することで、後続のミドルウェアは request.context.logger を使用することができます。ビジネスロジック(mainHandler)の引数は event と context のみですが、context.logger を使用可能となります。

### 4-2. パイプラインの実装

#### miggy4.js

```js:miggy4.js
const defaultMainHandler = () => {};

const MiddlewareFramework = (mainHandler = defaultMainHandler) => {
  const beforeMiddlewares = [];
  const afterMiddlewares = [];
  const onErrorMiddlewares = [];

  const lambdaHandler = async (event = {}, context = {}) => {
    const request = {
      event,
      context,
      response: undefined,
      error: undefined,
    };

    try {
      // 前処理を実行する
      await runMiddlewares(request, [...beforeMiddlewares]);

      // メイン処理に行く前にすでにresponseがある場合はスキップ
      if (typeof request.response === "undefined") {
        // メイン処理
        request.response = await mainHandler(request.event, request.context);

        // 後処理を実行する
        await runMiddlewares(request, [...afterMiddlewares]);
      }
    } catch (error) {
      request.error = error;

      throw request.error;
    }

    return request.response;
  };

  lambdaHandler.use = (middlewares) => {
    if (!Array.isArray(middlewares)) {
      middlewares = [middlewares];
    }
    for (const middleware of middlewares) {
      const { before, after, onError } = middleware;

      if (!before && !after && !onError) {
        throw new Error(
          '"before", "after", "onError" キーのいずれかでミドルウェア関数を登録して下さい'
        );
      }

      if (before) lambdaHandler.before(before);
      if (after) lambdaHandler.after(after);
      if (onError) lambdaHandler.onError(onError);
    }
    return lambdaHandler;
  };

  lambdaHandler.before = (beforeMiddleware) => {
    // FIFO
    beforeMiddlewares.push(beforeMiddleware);
    return lambdaHandler;
  };
  lambdaHandler.after = (afterMiddleware) => {
    // FILO
    afterMiddlewares.unshift(afterMiddleware);
    return lambdaHandler;
  };
  lambdaHandler.onError = (onErrorMiddleware) => {
    // FILO
    onErrorMiddlewares.unshift(onErrorMiddleware);
    return lambdaHandler;
  };

  return lambdaHandler;
};

const runMiddlewares = async (request, middlewares) => {
  for (const nextMiddleware of middlewares) {
    const res = await nextMiddleware(request);
    // 既にレスポンスが決まっていれば終了する
    if (typeof res !== "undefined") {
      request.response = res;
      return;
    }
  }
};

module.exports.miggy = MiddlewareFramework;
```

index4.js にパイプラインのテストコードを実装します。

- event.body を文字列から JSON オブジェクトに変換する前処理ミドルウェアを追加します。
- レスポンスを編集する後処理ミドルウェアを追加します。

#### index4.js

```js:index4.js
const { miggy } = require("./miggy4.js");

const businessLogic = async (event, context) => {
  console.log("ビジネスロジック実行中");

  const result = {
    title: "<<< ビジネスロジック >>>",
    message: event.body.artist + "さんこんにちは！",
    event,
    context,
  };

  return result;
};

// 文字列をJSONに変換
const middleware_json = (request) => {
  try {
    request.event.body = JSON.parse(request.event.body);
  } catch (error) {
    throw error;
  }
};

// レスポンスを編集
const middleware_response = (request) => {
  try {
    const response = request.response;
    if (response.title) delete response.title;
    if (response.event) delete response.event;
    if (response.context) delete response.context;
    response.message = "+++ " + response.message + " +++";
  } catch (error) {
    throw error;
  }
};

const handler = miggy(businessLogic)
  .use({ before: middleware_json })
  .use({ after: middleware_response });

exports.handler = handler;

const event = { body: '{"artist":"米酢元気","song":"Lemon酢"}' };
// 失敗するeventも準備
//const event = { body: { artist:"米酢元気", song:"Lemon酢"} };
const context = { isContext: true };

handler(event, context)
  .then((result) => {
    console.log("handlerのresult: ", result);
  })
  .catch((error) => {
    console.log("error", error.message);
  });
```

#### ローカルで実行してパイプライン連携の結果を確認します。

```sh
$ node index4.js
ビジネスロジック実行中
handlerのresult:  { message: '+++ 米酢元気さんこんにちは！ +++' }
miggy $
```

---
（１）はここまでとなります。
基本中の基本はここまででも十分理解出来たかと思いますが、次もあります！
[AWS Lambda 用ミドルウェアフレームワークの作成方法を middy を題材に検証（２）](https://qiita.com/Katsumi1972/items/d8e89d347a3f1bd11d4f)