なぜ「Middy」なのか──価値の源泉は“定型の外出し”にある
Lambdaでは、入力整形やバリデーション、認可、エラーハンドリング、CORS、ログ出力などの“横断的関心事”がハンドラ内に散らばりがちです。「Middy」はこれらをミドルウェアとして合成する仕組みを提供し、コードの重複を除去します。公式サイトでも「Node.js向けのAWS Lambdaミドルウェアエンジン」と明確に位置づけられており、目的がクリアです。
引用:Middy公式サイト
Middy is a Node.js middleware engine for AWS Lambda that lets you organise your Lambda code, remove code duplication, and focus on business logic!
また、v5以降はESMへ移行しており、CommonJSは非推奨になりました。LambdaはESMとtop-level awaitをサポートしているため、相性は良好です。
TypeScript + Middy の基本セットアップ
まずは「API Gateway + JSON」の最小構成です。TypeScriptの型で入出力を固め、ミドルウェアで“ハンドラ以外”を組み立てます。
// package.json は "type": "module" を推奨(Middy v5+はESM)
// npm i @middy/core @middy/http-json-body-parser @middy/http-error-handler @middy/validator
// npm i -D @types/aws-lambda
import middy from '@middy/core'
import httpJsonBodyParser from '@middy/http-json-body-parser'
import httpErrorHandler from '@middy/http-error-handler'
import validator from '@middy/validator'
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda'
// JSON SchemaはAJV準拠
const bodySchema = {
type: 'object',
required: ['name'],
properties: {
name: { type: 'string', minLength: 1 }
},
additionalProperties: false
} as const
type HandlerEvent = APIGatewayProxyEventV2 & { body: { name: string } }
const base = async (event: HandlerEvent): Promise<APIGatewayProxyStructuredResultV2> => {
const message = `hello, ${event.body.name}`
return { statusCode: 200, body: JSON.stringify({ message }) }
}
export const handler = middy(base)
.use(httpJsonBodyParser()) // bodyを自動でObject化
.use(validator({ eventSchema: { body: bodySchema } })) // 入力検証
.use(httpErrorHandler()) // 例外をHTTPレスポンス化
http-json-body-parser
はJSONボディを安全にパースし、壊れたJSONはhttp-error-handler
と併用すると415として扱えます。validator
はAJVベースで入力・出力を検証でき、エラーメッセージのローカライズにも対応しています。
SSM/Secretsの安全な取得を「1行」で
認証情報や設定値は環境変数に置かず、呼び出し時にフェッチしキャッシュするのが安全です。
// npm i @middy/ssm @middy/secrets-manager
import ssm from '@middy/ssm'
import secretsManager from '@middy/secrets-manager'
export const handler = middy(base)
.use(secretsManager({
fetchData: { apiToken: 'prod/api_token' },
setToContext: true
}))
.use(ssm({
fetchData: { CONFIG_JSON: '/prod/app/config' },
setToContext: true
}))
.use(httpJsonBodyParser())
.use(validator({ eventSchema: { body: bodySchema } }))
.use(httpErrorHandler())
// 以降 handler の第2引数 context.CONFIG_JSON / context.apiToken が使える
@middy/secrets-manager
と@middy/ssm
は、指定名やパスから値を取得しcontext
へ注入できます。
代表的プラグインの実装レシピ
ここからは“使う頻度が高い”プラグインのTypeScript例です。プロジェクトの土台に組み込むと、以降のハンドラはほぼドメインロジックだけで書けます。
CORSとヘッダ正規化
フロントエンド直結のAPIではCORS設定を忘れずに。
// npm i @middy/http-cors
import cors from '@middy/http-cors'
export const handler = middy(base)
.use(cors({ origins: ['https://app.example.com'] }))
.use(httpJsonBodyParser())
.use(httpErrorHandler())
http-cors
はAccess-Control-Allow-*
系ヘッダをセットします。CORSはミドルウェアの適用順によって期待通りに動かないことがあるため、先頭に置くのが実戦的です。
HTTPイベントの正規化
APIGatewayのイベント差異を吸収します。
// npm i @middy/http-event-normalizer
import httpEventNormalizer from '@middy/http-event-normalizer'
export const handler = middy(base).use(httpEventNormalizer())
HTTPイベントの欠落フィールドを補完するユーティリティです。非HTTPイベントに適用すると想定外の動作になるため“用途限定”で使います。
マルチパートのボディ解析(ファイルアップロード)
フロントのフォームから直接アップロードする場合の基礎です。
// npm i @middy/http-multipart-body-parser
import httpMultipartBodyParser from '@middy/http-multipart-body-parser'
export const handler = middy(base).use(httpMultipartBodyParser())
multipart/form-data
をパースしてオブジェクト化します。バリデータと組み合わせて使うと堅牢になります。
SQSの「部分失敗」を安全に扱う
バッチ処理で“落ちた分だけ再試行”したいときの定番です。
// npm i @middy/sqs-partial-batch-failure
import middy from '@middy/core'
import sqsBatch from '@middy/sqs-partial-batch-failure'
import type { SQSBatchResponse, SQSEvent } from 'aws-lambda'
const base = async (event: SQSEvent): Promise<SQSBatchResponse> => {
const results = await Promise.allSettled(event.Records.map(async (r) => {
// レコード単位の処理
}))
// ミドルウェアがfailed分を自動で batchItemFailures に落とす
return { batchItemFailures: [] }
}
export const handler = middy(base).use(sqsBatch())
このミドルウェアを使う場合、Event Source MappingのFunctionResponseTypes
にReportBatchItemFailures
を設定しておく必要があります。
Powertoolsでログ・メトリクス・トレースを“フレームワーク化”
「ログは構造化JSON」「メトリクスはEMF」「トレースはX-Ray」を組織の標準にしてしまうとレビューが速くなります。PowertoolsにはMiddy連携があり、ミドルウェアとして差し込めます。
// npm i @aws-lambda-powertools/logger @aws-lambda-powertools/metrics @aws-lambda-powertools/tracer
import { Logger } from '@aws-lambda-powertools/logger'
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics'
import { Tracer } from '@aws-lambda-powertools/tracer'
const logger = new Logger({ serviceName: 'orders' })
const metrics = new Metrics({ namespace: 'App' })
const tracer = new Tracer({ serviceName: 'orders' })
const base = async () => {
logger.info('received')
metrics.addMetric('Processed', MetricUnit.Count, 1)
return { statusCode: 200, body: 'ok' }
}
export const handler = middy(base)
// ここにPowertoolsのMiddy対応を差し込む(公式の連携ガイドを参照)
Powertools TSはEMFでCloudWatchメトリクスを出力し、Middyとの統合ガイドも用意されています。
「イベントを自動でパースしてくれる」はどこまで本当か
MiddyにはHTTP系のhttp-json-body-parser
やhttp-event-normalizer
に加え、各種AWSイベントの“正規化”を担う@middy/event-normalizer
も存在します。Amazon DynamoDBやAWS SQSなどのイベントを解析・正規化するユーティリティで、イベントの差異を吸収できます。ただしプロジェクト適用前に対象イベントでの挙動を実機テストするのが安全です。
言語サポートの整理──MiddyはNode.js専用、他言語は代替を選ぶ
Middyは「Node.jsのためのLambdaミドルウェアエンジン」です。つまり公式にサポートする言語はJavaScript/TypeScriptであり、PythonやJavaでは別の選択肢(たとえば各言語版のAWS Lambda Powertools)を検討します。この前提を押さえておくと、ランタイム選定の議論が迷子になりません。
デメリットと運用上の注意点(実務視点)
導入効果が大きい一方で、いくつかの落とし穴があります。ここは最初に合意しておくと後戻りしません。
カテゴリ | 詳細 |
---|---|
v5以降のESM移行 |
|
ミドルウェアの適用 |
|
パフォーマンス |
|
型安全 |
|
現場で使う「ベースミドルウェア」の設計例
毎回ハンドラでuse()
を並べるのは冗長なので、ベースを一箇所に寄せます。レビュー効率と一貫性が段違いに上がります。
// middlewares/base.ts
import middy from '@middy/core'
import httpErrorHandler from '@middy/http-error-handler'
import httpJsonBodyParser from '@middy/http-json-body-parser'
import cors from '@middy/http-cors'
export const withBase = <E, R>(fn: (e: E) => Promise<R>) =>
middy(fn)
.use(cors({ origins: ['https://app.example.com'] }))
.use(httpJsonBodyParser())
.use(httpErrorHandler())
// handlers/hello.ts
import { withBase } from '../middlewares/base'
export const handler = withBase(async () => ({ statusCode: 200, body: 'ok' }))
この「共通レイヤ」はチームの合意形成のポイントで、セキュリティポリシーや監査要件もここに寄せます。公式ドキュメントの各プラグイン説明は簡潔なので、ドメインに合わせて拡張していくのが良いです。
主要ミドルウェアのマッピング一覧
以下は「最初に何を入れるか」を考えるときの思考を整理したものです。
表 公式ミドルウェアの用途と注意点の早見表
パッケージ | 主な用途 | 注意点・補足 |
---|---|---|
| JSONボディの自動パース | 415の扱いは |
| 例外をHTTPレスポンスへ変換 |
|
| CORSヘッダの付与 | 先頭に置くと安定。Serverless FrameworkのCORS設定との二重管理に注意 |
| 入出力のJSON Schema検証 | AJVベース。ローカライズやメッセージの上書きが可能 |
| 設定・秘匿情報の取得と | キャッシュ・名前付け規約に留意。Secretsはバッチ取得不可のため個別リクエストになる |
| SQSの部分失敗処理 | ESMの設定とEvent Source Mappingの |
表は、初期導入で迷いがちな公式プラグインの「用途と注意点」を整理したものです。イベントや運用要件に応じて採用の有無を判断して下さい。
実用ユースケース別スニペット集
1. Webhook受信(認証付きJSON、共通ログ)
外部SaaSのWebhookは“整形→検証→ドメイン処理→例外変換”の一本道にすると堅牢になります。
import middy from '@middy/core'
import json from '@middy/http-json-body-parser'
import errors from '@middy/http-error-handler'
import validator from '@middy/validator'
import cors from '@middy/http-cors'
import { Logger } from '@aws-lambda-powertools/logger'
const logger = new Logger({ serviceName: 'webhook' })
const base = async (evt: any) => {
logger.appendKeys({ requestId: evt.requestContext?.requestId })
if (evt.headers['x-api-key'] !== process.env.WEBHOOK_TOKEN) {
throw Object.assign(new Error('unauthorized'), { statusCode: 401 })
}
// ... domain logic
return { statusCode: 204, body: '' }
}
export const handler = middy(base)
.use(cors({ origins: ['https://partner.example.com'] }))
.use(json())
.use(validator({ eventSchema: { body: { type:'object' } } }))
.use(errors())
Powertools
のLoggerを並走させると、構造化ログで後段の可観測性が一気に上がります。
2. S3プレサイン生成(SSMで動的設定)
環境差分をSSMに寄せて、コードは不変に保ちます。
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import ssm from '@middy/ssm'
import middy from '@middy/core'
const client = new S3Client({})
const base = async (_evt: any, ctx: any) => {
const bucket = ctx.CONFIG_JSON?.uploadBucket
// ここで@aws-sdk/s3-request-presigner等を使ってURL生成
return { statusCode: 200, body: JSON.stringify({ bucket }) }
}
export const handler = middy(base).use(ssm({
fetchData: { CONFIG_JSON: '/prod/app/config' },
setToContext: true
}))
SSMミドルウェアはパスや名前指定でまとめて取得でき、context
へ大文字キーなどで注入されます。
3. SQSバッチ処理(部分失敗 + バリデーション)
“壊れたメッセージを正しく再試行させる”のがポイントです。
import middy from '@middy/core'
import sqsBatch from '@middy/sqs-partial-batch-failure'
import validator from '@middy/validator'
import type { SQSEvent, SQSBatchResponse } from 'aws-lambda'
const base = async (event: SQSEvent): Promise<SQSBatchResponse> => {
// ... allSettled + 個別エラーハンドリング
return { batchItemFailures: [] }
}
export const handler = middy(base)
.use(validator({ eventSchema: { body: { type: 'object' } } }))
.use(sqsBatch())
Event Source MappingにReportBatchItemFailures
を設定することを忘れずに。
個人的な所感──「ミドルウェアで語彙を合わせる」ことが最大の効用
プロジェクトでは「共通の語彙」を作るほどレビューとQoSが安定します。Middyは「語彙=ミドルウェア」として共通化しやすいのが強みです。
たとえば「HTTP系はjson→validator→errors」「バッチはvalidator→sqs-partial」「設定はssm/secretsをcontextに生やす」「可観測性はPowertoolsを標準搭載」という合意があるだけで、追加のLambdaはハンドラだけを書けばよくなります。ESM移行や型整合は確かにハードルですが、一度踏み切るとLambda開発が「アプリ設計」のフェーズに引き上がります。
これは現場の生産性だけでなく、保守品質を長期的に押し上げる効果があります。
まとめ
本記事では、TypeScriptとMiddyの実装手順から主要プラグインの実例、デメリットの現実解までを通しで整理しました。Middyは“Lambdaの作法”をチームで共有するための土台として非常に優れており、メトリクス・ログ・トレースのフレームワーク化まで踏み込むと運用品質が段違いに上がります。
新規LambdaはまずMiddyを前提に設計し、既存関数もESM移行のタイミングで段階的に統一していくのが現実的だと考えます。