CLOVER🍀

That was when it all began.

AWS Lambda向けのミドルウェアエンジンmiddyを試す(Amazon API Gateway、LocalStack)

これは、なにをしたくて書いたもの?

AWS Lambda関数(Node.js)向けの、middyというライブラリの存在を知ったので、ちょっと試してみることにしました。

middy

ライブラリと書きましたが、middyは「AWS Lambda関数を簡単に書くためのミドルウェアエンジンだ」という
説明をしています。

Middy is a very simple middleware engine that allows you to simplify your AWS Lambda code when using Node.js.

middyのWebサイトおよびリポジトリはこちら。

middy | 🛵 The stylish Node.js middleware engine for AWS Lambda

GitHub - middyjs/middy: 🛵 The stylish Node.js middleware engine for AWS Lambda

middyが作られた動機は、こちらに書かれています。

middy / Why?

AWS Lambda関数でのコードを書く際には、入力の解析、バリデーション、エラーハンドリングなどビジネスロジック以外にも
対処する必要がある技術的課題があります。

Anyway, when you are writing a handler, you still have to deal with some common technical concerns outside business logic, like input parsing and validation, output serialization, error handling, etc.

これは必要なものですが、ビジネスロジックを汚染してしまい、コードの読みやすさや保守性を難しくしてしまいます。

(Node.jsにおける)一般的なWebフレームワーク、たとえばfastify、hapi、expressなどはこれをミドルウェアパターンを
用いて解決しています。

ですが、AWS Lambda関数環境下で同様のアプローチを実現しているものがなかったので、middyを作ったようです。

middyの雰囲気を理解するためのコード例は、こちら。

middy / Usage

middyとミドルウェア

middyはハンドラーとなる関数をmiddy関数でラップすることで、ミドルウェアの追加を可能にします。

middy / How it works

ミドルウェアの実行フェーズにはbeforeとafterがあり、それぞれハンドラー関数の前後で実行されます。
ドキュメントからの抜粋ですが、3つのミドルウェアを追加した場合、実行順は以下になります。

  • middleware1 (before)
  • middleware2 (before)
  • middleware3 (before)
  • handler
  • middleware3 (after)
  • middleware2 (after)
  • middleware1 (after)

afterは逆順ですね。

また、ミドルウェアは関数の実行を中止してすぐにレスポンスを返したり、エラーハンドリングも行うことが
できます。

ミドルウェアの書き方については、こちら。

middy / Writing a middleware

middyには実装済みのミドルウェアがいくつかあり、middyのリポジトリに含まれれるものは現時点で以下になります。

middy / Available middlewares

  • middy / Misc
    • error-logger: Logs errors
    • input-output-logger: Logs request and response
    • do-not-wait-for-empty-event-loop: Sets callbackWaitsForEmptyEventLoop property to false
    • cloudwatch-metrics: Hydrates lambda’s context.metrics property with an instance of AWS MetricLogger
    • warmup: Used to pre-warm a lambda function
  • middy / Request Transformation
    • http-content-negotiation: Parses Accept-* headers and provides utilities for content negotiation (charset, encoding, language and media type) for HTTP requests
    • http-header-normalizer: Normalizes HTTP header names to their canonical format
    • http-json-body-parser: Automatically parses HTTP requests with JSON body and converts the body into an object. Also handles gracefully broken JSON if used in combination of httpErrorHandler.
    • http-multipart-body-parser: Automatically parses HTTP requests with content type multipart/form-data and converts the body into an object.
    • http-urlencode-body-parser: Automatically parses HTTP requests with URL encoded body (typically the result of a form submit).
    • http-urlencode-path-parser: Automatically parses HTTP requests with URL encoded path.
    • s3-key-normalizer: Normalizes key names in s3 events.
    • sqs-json-body-parser: Parse body from SQS events
    • validator: Automatically validates incoming events and outgoing responses against custom schemas
  • middy / Response Transformation
    • http-cors: Sets HTTP CORS headers on response
    • http-error-handler: Creates a proper HTTP response for errors that are created with the http-errors module and represents proper HTTP errors.
    • http-event-normalizer: Normalizes HTTP events by adding an empty object for queryStringParameters, multiValueQueryStringParameters or pathParameters if they are missing.
    • http-security-headers: Applies best practice security headers to responses. It’s a simplified port of HelmetJS.
    • http-partial-response: Filter response objects attributes based on query string parameters.
    • http-response-serializer: HTTP response serializer.
    • sqs-partial-batch-failure: handles partially failed SQS batches.
  • middy / Fetch Data
    • rds-signer: Fetches token for connecting to RDS with IAM users.
    • s3-object-response: Gets and write S3 object response.
    • secrets-manager: Fetches parameters from AWS Secrets Manager.
    • ssm: Fetches parameters from AWS Systems Manager Parameter Store.
    • sts: Fetches credentials to assumes IAM roles for connection to other AWS services.

リクエスト、レスポンスに関するものから、AWSのサービスを利用するものまでありますね。

この他、コミュニティによるミドルウェアもあります。

middy / Community generated middleware

説明はこれくらいにして、とりあえず使ってみましょう。

環境

今回の環境はこちら。LocakStackを使います。

$ localstack --version
0.13.2.1


$ python3 -V
Python 3.8.10


$ awslocal --version
aws-cli/2.4.8 Python/3.8.8 Linux/5.4.0-91-generic exe/x86_64.ubuntu.20 prompt/off


$ samlocal --version
SAM CLI, version 1.36.0

LocalStackの起動。

$ LAMBDA_EXECUTOR=docker-reuse localstack start

アプリケーションは、Node.js+TypeScriptで書くことにします。

$ node --version
v14.18.2


$ npm --version
6.14.15

middyでHello World

最初に、middyを使ってHello World的なAWS Lambda関数を作成しましょう。この後で、AWS Lambda関数を追加することに
します。

AWS SAMでプロジェクト作成。

$ samlocal init

Node.js 14.xを選びます。

Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1
What package type would you like to use?
        1 - Zip (artifact is a zip uploaded to S3)
        2 - Image (artifact is an image uploaded to an ECR image repository)
Package type: 1

Which runtime would you like to use?
        1 - nodejs14.x
        2 - python3.9
        3 - ruby2.7
        4 - go1.x
        5 - java11
        6 - dotnetcore3.1
        7 - nodejs12.x
        8 - nodejs10.x
        9 - python3.8
        10 - python3.7
        11 - python3.6
        12 - python2.7
        13 - ruby2.5
        14 - java8.al2
        15 - java8
        16 - dotnetcore2.1
Runtime: 1

Project name [sam-app]: middy-getting-started

Cloning from https://github.com/aws/aws-sam-cli-app-templates

AWS quick start application templates:
        1 - Hello World Example
        2 - Step Functions Sample App (Stock Trader)
        3 - Quick Start: From Scratch
        4 - Quick Start: Scheduled Events
        5 - Quick Start: S3
        6 - Quick Start: SNS
        7 - Quick Start: SQS
        8 - Quick Start: Web Backend
Template selection: 1

    -----------------------
    Generating application:
    -----------------------
    Name: middy-getting-started
    Runtime: nodejs14.x
    Architectures: x86_64
    Dependency Manager: npm
    Application Template: hello-world
    Output Directory: .

    Next application steps can be found in the README file at ./middy-getting-started/README.md


    Commands you can use next
    =========================
    [*] Create pipeline: cd middy-getting-started && sam pipeline init --bootstrap
    [*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch

プロジェクト内に移動。

$ cd middy-getting-started

いきなりですが、生成されたテンプレートは要らないので削除します。

$ rm -rf hello-world

別のディレクトリを作って移動。

$ mkdir hello-middy
$ cd hello-middy

この中で、Node.jsプロジェクトとして初期化します。

$ npm init -y
$ npm i -D typescript
$ npm i -D -E prettier
$ mkdir src

middyを使うには、最低限@middy/coreをインストールすればよいみたいです。TypeScriptで使う場合には、
@types/aws-lambdaも追加します。

$ npm i @middy/core
$ npm i -D @types/node@v14 @types/aws-lambda

middy / Install

今回の依存関係は、このようになりました。

  "devDependencies": {
    "@types/aws-lambda": "^8.10.89",
    "@types/node": "^14.18.5",
    "prettier": "2.5.1",
    "typescript": "^4.5.4"
  },
  "dependencies": {
    "@middy/core": "^2.5.4"
  }

設定ファイル。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

.prettierrc.json

{
  "singleQuote": true
}

package.jsonscripts定義は、このようにしました。

  "scripts": {
    "build": "tsc --project .",
    "format": "prettier --write src"
  },

TypeScriptを使ったコード例は、middyにもサンプルがあるのでこちらを見つつ

middy / TypeScript

こんな感じで作成。

src/app.ts

import middy from '@middy/core';
import {
  APIGatewayProxyEvent,
  APIGatewayProxyResult,
  Context,
} from 'aws-lambda';

async function baseHandler(
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> {
  let message;

  if (event.queryStringParameters && event.queryStringParameters['message']) {
    message = event.queryStringParameters['message'];
  } else {
    message = 'World';
  }

  return {
    statusCode: 200,
    body: JSON.stringify({
      message: `Hello ${message}!!`,
    }),
  };
}

export const lambdaHandler = middy(baseHandler);

QueryStringでパラメーターを取れるようにしていますが、単純なAWS Lambda関数です。

ポイントは、ハンドラー関数をmiddy関数でラップしてexportしていることです。

export const lambdaHandler = middy(baseHandler);

次に、AWS SAMのテンプレートを修正します。生成された時点でこうでしたが、

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs14.x
      Architectures:
        - x86_64
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

これを以下のように修正。

Resources:
  HelloMiddyFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-middy/
      Handler: app.lambdaHandler
      Runtime: nodejs14.x
      Architectures:
        - x86_64
      Events:
        HelloMiddy:
          Type: Api
          Properties:
            Path: /hello
            Method: get
    Metadata:
      BuildMethod: makefile

Outputs:
  HelloMiddyApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello Middy function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"

あとで他のAWS Lambda関数も追加するので、sam buildで一発でビルドできるように最初からMakefileを用意しました。

Makefile

build-HelloMiddyFunction:
        npm ci
        npm run build
        cp package*.json dist
        cd dist && \
          npm ci --production && \
          cp -R * ${ARTIFACTS_DIR}

ひとつ上のディレクトリ(AWS SAMのテンプレートがあるディレクトリ)に戻って

$ cd ..

ビルド。

$ samlocal build

LocalStack上にAmazon S3バケットを作って

$ awslocal s3 mb s3://my-bucket

デプロイ。

$ samlocal deploy --stack-name my-stack --region us-east-1 --s3-bucket my-bucket

Amazon API GatewayREST APIのIDを取得して

$ REST_API_ID=$(awslocal apigateway get-rest-apis --query 'items[0].id' --output text)

動作確認。

$ curl http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/hello
{"message":"Hello World!!"}


$ curl http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/hello?message=Middy
{"message":"Hello Middy!!"}

OKですね。

少し、中身を見てみる

動くには動きましたが、middy関数がなにをしているのかがちょっと気になります。

@middy/coreソースコードを見ると、実質この1ファイルです。

https://github.com/middyjs/middy/blob/2.5.4/packages/core/index.js

ほぼ、ミドルウェアAWS Lambdaのハンドラー関数に追加するだけのコードですね。

となると、今回のサンプルはミドルウェアを使っていないので、middyを使わずに書いた関数とほぼ差がないことになります。

middyのミドルウェアを使ったAWS Lambda関数を書く

このまま終わると面白くないので、middyのミドルウェアを使った関数を書いてみましょう。

お題としては、JSONでリクエストを送り、その中身を演算して返すものにしましょう。リクエストの内容に対する
バリデーションも行います。

主に使用するのは、以下のミドルウェアにします。

HTTPボディをJSONでパースするミドルウェアと、バリデーションを行うミドルウェアですね。

ドキュメントを読んでいると、http-json-body-parserにはhttp-header-normalizerを、validatorにはhttp-error-handler
合わせて使った方が良さそうなので、こちらも追加します。

AWS Lamda関数用のディレクトリを作成。

$ mkdir calc
$ cd calc
$ npm init -y
$ npm i -D typescript
$ npm i -D -E prettier
$ mkdir src

必要な依存関係を追加。

$ npm i @middy/core @middy/http-header-normalizer @middy/http-json-body-parser @middy/validator @middy/http-error-handler
$ npm i -D @types/node@v14 @types/aws-lambda

依存関係は、最終的にはこうなりました。

  "devDependencies": {
    "@types/aws-lambda": "^8.10.89",
    "@types/node": "^14.18.5",
    "prettier": "2.5.1",
    "typescript": "^4.5.4"
  },
  "dependencies": {
    "@middy/core": "^2.5.4",
    "@middy/http-error-handler": "^2.5.4",
    "@middy/http-header-normalizer": "^2.5.4",
    "@middy/http-json-body-parser": "^2.5.4",
    "@middy/validator": "^2.5.4"
  }

tsconfig.js.prettierrc.jsonpackage.jsonscriptsの定義は最初と同じなので省略します。

ソースコードは、こんな感じで作成。

src/app.ts

import middy from '@middy/core';
import httpHeaderNormalizer from '@middy/http-header-normalizer';
import httpJsonBodyParser from '@middy/http-json-body-parser';
import httpErrorHandler from '@middy/http-error-handler';
import validator from '@middy/validator';
import {
  APIGatewayProxyEvent,
  APIGatewayProxyResult,
  Context,
} from 'aws-lambda';

const schema = {
  type: 'object',
  required: ['body'],
  properties: {
    body: {
      type: 'object',
      required: ['operator', 'a', 'b'],
      additionalProperties: false,
      properties: {
        operator: {
          type: 'string',
          enum: ['+', '-'],
        },
        a: {
          type: 'number',
        },
        b: {
          type: 'number',
        },
      },
    },
  },
};

type CalcRequest = {
  operator: string;
  a: number;
  b: number;
};

const handler = async (
  event: any,
  context: Context
): Promise<APIGatewayProxyResult> => {
  const calcRequest = event.body as CalcRequest;

  let result;

  switch (calcRequest.operator) {
    case '+':
      result = calcRequest.a + calcRequest.b;
      break;
    case '-':
      result = calcRequest.a - calcRequest.b;
      break;
  }

  return {
    statusCode: 200,
    body: JSON.stringify({
      result: result,
      originalBody: event.rawBody,
    }),
  };
};

export const lambdaHandler = middy(handler)
  .use(httpHeaderNormalizer())
  .use(httpJsonBodyParser())
  .use(validator({ inputSchema: schema }))
  .use(httpErrorHandler());

関数は、リクエストの内容で加減算できるものにしました。

const handler = async (
  event: any,
  context: Context
): Promise<APIGatewayProxyResult> => {
  const calcRequest = event.body as CalcRequest;

  let result;

  switch (calcRequest.operator) {
    case '+':
      result = calcRequest.a + calcRequest.b;
      break;
    case '-':
      result = calcRequest.a - calcRequest.b;
      break;
  }

  return {
    statusCode: 200,
    body: JSON.stringify({
      result: result,
      originalBody: event.rawBody,
    }),
  };
};

リクエストは、こんな感じで送ります。

{"operator": "+", "a": 5, "b": 3}

これを、JSONパース → バリデーションとつなぐため、ミドルウェアの適用順はこうなります。

export const lambdaHandler = middy(handler)
  .use(httpHeaderNormalizer())
  .use(httpJsonBodyParser())
  .use(validator({ inputSchema: schema }))
  .use(httpErrorHandler());

http-json-body-parserContent-Typeヘッダーがapplication/jsonでないと機能しませんが、その前にHTTPヘッダーを
正規化しておくことが推奨されているため、http-header-normalizerをその前に入れています。

ハンドラー関数に処理が来た時点では、JSONパースは終わっているのでそのままオブジェクトとして扱えます。

  const calcRequest = event.body as CalcRequest;

パース前のオリジナルの値はrawBodyに入っているので、今回はこれも返すようにしました。

    body: JSON.stringify({
      result: result,
      originalBody: event.rawBody,
    }),

また、JSONパースができたあとは、中身をバリデーションすることができます。このため、http-json-body-parserの後に
validatorが入ります。

最後のhttp-error-handlerは、バリデーションエラーが発生した時に400を返すことを目的に入れているミドルウェアです。
http-reponse-serializerを使っている場合を除き、http-error-handlerは最後に登録する必要があります。

バリデーションの定義はこちらです。

const schema = {
  type: 'object',
  required: ['body'],
  properties: {
    body: {
      type: 'object',
      required: ['operator', 'a', 'b'],
      additionalProperties: false,
      properties: {
        operator: {
          type: 'string',
          enum: ['+', '-'],
        },
        a: {
          type: 'number',
        },
        b: {
          type: 'number',
        },
      },
    },
  },
};

この内容は、validatorミドルウェアに設定する必要があります。
※ちなみに、リクエストだけではなくレスポンスもバリデーションすることができます

  .use(validator({ inputSchema: schema }))

バリデーションの定義はJSON Schemaで行い、バリデーションのエンジンとしてはAjvが使われています。

Ajv JSON schema validator

JSON Schema | The home of JSON Schema

JSON Schemaにはバージョンがあり、validatorプラグインはデフォルトではDraft 2019-09を使うAjvを利用するようなので

Default ajv plugins used: ajv-i18n, ajv-formats, ajv-formats-draft2019

Specification Links / Draft 2019-09 (formerly known as Draft 8)

JSON Schemaの書き方は、こちらを見ればよいことになります。

draft-handrews-json-schema-validation-02

あとはMakefileを作成して

Makefile

build-CalcFunction:
        npm ci
        npm run build
        cp package*.json dist
        cd dist && \
          npm ci --production && \
          cp -R * ${ARTIFACTS_DIR}

AWS SAMのテンプレートにも定義を追加します。

Resources:
  HelloMiddyFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-middy/
      Handler: app.lambdaHandler
      Runtime: nodejs14.x
      Architectures:
        - x86_64
      Events:
        HelloMiddy:
          Type: Api
          Properties:
            Path: /hello
            Method: get
    Metadata:
      BuildMethod: makefile
  CalcFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: calc/
      Handler: app.lambdaHandler
      Runtime: nodejs14.x
      Architectures:
        - x86_64
      Events:
        HelloMiddy:
          Type: Api
          Properties:
            Path: /calc
            Method: post
    Metadata:
      BuildMethod: makefile

Outputs:
  HelloMiddyApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello Middy function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  CalcApi:
    Description: "API Gateway endpoint URL for Prod stage for Calc function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/calc/"

ビルドして、Amazon S3バケットを作成して、デプロイ。

$ cd ..
$ samlocal build
$ awslocal s3 mb s3://my-bucket
$ samlocal deploy --stack-name my-stack --region us-east-1 --s3-bucket my-bucket
$ REST_API_ID=$(awslocal apigateway get-rest-apis --query 'items[0].id' --output text)

確認。

$ curl -XPOST -H 'Content-type: application/json' http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/calc -d '{"operator": "+", "a": 5, "b": 3}'
{"result":8,"originalBody":"{\"operator\": \"+\", \"a\": 5, \"b\": 3}"}


$ curl -XPOST -H 'Content-type: application/json' http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/calc -d '{"operator": "-", "a": 8, "b": 4}'
{"result":4,"originalBody":"{\"operator\": \"-\", \"a\": 8, \"b\": 4}"}

OKですね。

バリデーションが通らないリクエストを投げてみましょう。

$ curl -i -XPOST -H 'Content-type: application/json' http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/calc -d '{"operator": "*", "b": "hoge"}'
HTTP/1.1 400 
Content-Type: text/plain
Content-Length: 30
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: HEAD,GET,PUT,POST,DELETE,OPTIONS,PATCH
Access-Control-Allow-Headers: authorization,cache-control,content-length,content-md5,content-type,etag,location,x-amz-acl,x-amz-content-sha256,x-amz-date,x-amz-request-id,x-amz-security-token,x-amz-tagging,x-amz-target,x-amz-user-agent,x-amz-version-id,x-amzn-requestid,x-localstack-target,amz-sdk-invocation-id,amz-sdk-request
Access-Control-Expose-Headers: etag,x-amz-version-id
Connection: close
date: Wed, 05 Jan 2022 11:14:12 GMT
server: hypercorn-h11

Event object failed validation

結果は物悲しいですが、HTTPステータスコード400になりました。

ちなみに、http-error-handlerミドルウェアを入れていない場合はこれで500(Internal Server Error)になります。

エラーの理由はログに出るようになっていて

$ awslocal logs tail /aws/lambda/my-stack-CalcFunction-f9323496

こんな感じになっています。

2022-01-05T11:14:12.825000+00:00 2022/01/05/[LATEST]758beacc 2022-01-05T11:14:12.817Z   241e7011-db28-10df-08c2-3a8f653e111c    ERROR   BadRequestError: Event object failed validation
2022-01-05T11:14:12.825000+00:00 2022/01/05/[LATEST]758beacc     at createError (/var/task/node_modules/@middy/util/index.js:259:10)
2022-01-05T11:14:12.826000+00:00 2022/01/05/[LATEST]758beacc     at validatorMiddlewareBefore (/var/task/node_modules/@middy/validator/index.js:53:21)
2022-01-05T11:14:12.827000+00:00 2022/01/05/[LATEST]758beacc     at runMiddlewares (/var/task/node_modules/@middy/core/index.js:120:88)
2022-01-05T11:14:12.828000+00:00 2022/01/05/[LATEST]758beacc     at async runRequest (/var/task/node_modules/@middy/core/index.js:80:5) {
2022-01-05T11:14:12.829000+00:00 2022/01/05/[LATEST]758beacc   details: [
2022-01-05T11:14:12.830000+00:00 2022/01/05/[LATEST]758beacc     {
2022-01-05T11:14:12.831000+00:00 2022/01/05/[LATEST]758beacc       instancePath: '/body',
2022-01-05T11:14:12.832000+00:00 2022/01/05/[LATEST]758beacc       schemaPath: '#/properties/body/required',
2022-01-05T11:14:12.833000+00:00 2022/01/05/[LATEST]758beacc       keyword: 'required',
2022-01-05T11:14:12.834000+00:00 2022/01/05/[LATEST]758beacc       params: [Object],
2022-01-05T11:14:12.835000+00:00 2022/01/05/[LATEST]758beacc       message: 'must have required property a'
2022-01-05T11:14:12.836000+00:00 2022/01/05/[LATEST]758beacc     },
2022-01-05T11:14:12.837000+00:00 2022/01/05/[LATEST]758beacc     {
2022-01-05T11:14:12.838000+00:00 2022/01/05/[LATEST]758beacc       instancePath: '/body/operator',
2022-01-05T11:14:12.838000+00:00 2022/01/05/[LATEST]758beacc       schemaPath: '#/properties/body/properties/operator/enum',
2022-01-05T11:14:12.839000+00:00 2022/01/05/[LATEST]758beacc       keyword: 'enum',
2022-01-05T11:14:12.840000+00:00 2022/01/05/[LATEST]758beacc       params: [Object],
2022-01-05T11:14:12.841000+00:00 2022/01/05/[LATEST]758beacc       message: 'must be equal to one of the allowed values'
2022-01-05T11:14:12.842000+00:00 2022/01/05/[LATEST]758beacc     },
2022-01-05T11:14:12.843000+00:00 2022/01/05/[LATEST]758beacc     {
2022-01-05T11:14:12.844000+00:00 2022/01/05/[LATEST]758beacc       instancePath: '/body/b',
2022-01-05T11:14:12.845000+00:00 2022/01/05/[LATEST]758beacc       schemaPath: '#/properties/body/properties/b/type',
2022-01-05T11:14:12.846000+00:00 2022/01/05/[LATEST]758beacc       keyword: 'type',
2022-01-05T11:14:12.847000+00:00 2022/01/05/[LATEST]758beacc       params: [Object],
2022-01-05T11:14:12.848000+00:00 2022/01/05/[LATEST]758beacc       message: 'must be number'
2022-01-05T11:14:12.849000+00:00 2022/01/05/[LATEST]758beacc     }
2022-01-05T11:14:12.850000+00:00 2022/01/05/[LATEST]758beacc   ]
2022-01-05T11:14:12.851000+00:00 2022/01/05/[LATEST]758beacc }

レスポンスにもある程度戻したいと思うのですが…今回はパスです。

また、JSONとしておかしい文字列を渡すと422になります。

$ curl -i -XPOST -H 'Content-type: application/json' http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/calc -d '{"operator": '
HTTP/1.1 422 
Content-Type: text/plain
Content-Length: 61
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: HEAD,GET,PUT,POST,DELETE,OPTIONS,PATCH
Access-Control-Allow-Headers: authorization,cache-control,content-length,content-md5,content-type,etag,location,x-amz-acl,x-amz-content-sha256,x-amz-date,x-amz-request-id,x-amz-security-token,x-amz-tagging,x-amz-target,x-amz-user-agent,x-amz-version-id,x-amzn-requestid,x-localstack-target,amz-sdk-invocation-id,amz-sdk-request
Access-Control-Expose-Headers: etag,x-amz-version-id
Connection: close
date: Wed, 05 Jan 2022 11:17:33 GMT
server: hypercorn-h11

Content type defined as JSON but an invalid JSON was provided

これもエラーを発生させるのはhttp-json-body-parserですが、ハンドリングしているのはhttp-error-handlerなので、
こちらを入れておかないと500エラーになります。

とりあえず、こんな感じでしょうか。

まとめ

AWS Lambda向けのミドルウェアエンジン、middyを試してみました。

軽量でミドルウェアの組み合わせで扱えるので、Expressのような大きなフレームワークよりAWS Lambda環境に
向いているのかなと思います。

また、ひとつひとつのミドルウェアは小さいので、困ったらソースコードを見た方が確実に早いですね。
自分も今回書きながら何度も眺めることになりました。