TypeScriptで始めるAWS Lambda「Middy」実践ガイド──プラグイン設計でコードを半分にする

TypeScriptで始めるAWS Lambda「Middy」実践ガイド──プラグイン設計でコードを半分にする

エンジニアブログ
最終更新日:2025年08月26日公開日:2025年08月16日
柳澤 大志
writer:柳澤 大志
XThreads

Middy」はAWS Lambda(Node.js)のためのミドルウェアエンジンで、ハンドラ周辺の定型処理をプラグインとして切り出し、ビジネスロジックに集中できる開発体験を提供します。TypeScriptと組み合わせると、入力スキーマの検証、HTTPエラー整形、ボディの自動パース、SQSの部分失敗処理、Secrets/SSMの安全な取得などを数行で組み込み可能です。

さらにAWS公式の「Powertools」との統合でログ・メトリクス・トレースを「フレームワーク化」できます。本記事では、現場投入を前提に、TypeScriptでの実装手順、主要プラグインのコード例、言語サポートの位置づけ、そしてあえて押さえておくべきデメリットまでをまとめて解説します。

MiddyはLambda実装のデファクトだと考えており、使わない理由は見当たりません。導入してからは、もうMiddy無しで全体実装を進めたいと思わなくなりました。もちろん、どこまでをミドルウェアに寄せるかの設計判断こそが価値の差になります。

なぜ「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-corsAccess-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のFunctionResponseTypesReportBatchItemFailuresを設定しておく必要があります。

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-parserhttp-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移行

  • v5以降はESMが前提であるため、既存のCommonJS資産の移行コストが発生
  • 移行計画には「tsconfig」「bundler設定」「LambdaランタイムのESM化」といったタスクの考慮が必要
  • AWSはLambdaにおけるES Modulesおよびtop-level awaitをサポートしているため、技術的な前提は満たされているが、アプリケーション側のリファクタリングが求められる

ミドルウェアの適用

  • ミドルウェアの適用順序やイベントの適合性を誤ると、予期せぬ不具合を招く可能性がある
  • http-event-normalizerのような一部のミドルウェアは、非HTTPイベントに適用するとエラーを引き起こす可能性があるため、用途を限定して使用する必要がある

パフォーマンス

  • プラグインを過剰に導入すると、特にコールドスタート時における初期化コストが増加する可能性がある
  • ミドルウェアは必要最小限の構成に留めるべきである
  • AWS Lambda Powertoolsのような汎用的な機能は、ベースミドルウェアとして共通化することが推奨される

型安全

  • validatorミドルウェアを利用していても、実装次第で型安全性が損なわれる場合がある
  • 具体的には、validatorが用いるJSON SchemaとTypeScriptの型定義が乖離する可能性がある
  • この問題を回避するため、json-schema-to-tsのようなツールを併用して型定義を自動生成する、あるいは型とスキーマを同時に扱う共通のラッパー関数を導入するなどの対策を検討すべき

現場で使う「ベースミドルウェア」の設計例

毎回ハンドラで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' }))

この「共通レイヤ」はチームの合意形成のポイントで、セキュリティポリシーや監査要件もここに寄せます。公式ドキュメントの各プラグイン説明は簡潔なので、ドメインに合わせて拡張していくのが良いです。

主要ミドルウェアのマッピング一覧

以下は「最初に何を入れるか」を考えるときの思考を整理したものです。

表 公式ミドルウェアの用途と注意点の早見表

パッケージ

主な用途

注意点・補足

@middy/http-json-body-parser

JSONボディの自動パース

415の扱いはhttp-error-handler併用で整う。バリデータ前段に置くと取り回しが良い

@middy/http-error-handler

例外をHTTPレスポンスへ変換

http-errorsと相性が良い。ステータス<500はexposeの扱いあり

@middy/http-cors

CORSヘッダの付与

先頭に置くと安定。Serverless FrameworkのCORS設定との二重管理に注意

@middy/validator

入出力のJSON Schema検証

AJVベース。ローカライズやメッセージの上書きが可能

@middy/ssm / @middy/secrets-manager

設定・秘匿情報の取得とcontext注入

キャッシュ・名前付け規約に留意。Secretsはバッチ取得不可のため個別リクエストになる

@middy/sqs-partial-batch-failure

SQSの部分失敗処理

ESMの設定とEvent Source MappingのReportBatchItemFailuresを忘れない

表は、初期導入で迷いがちな公式プラグインの「用途と注意点」を整理したものです。イベントや運用要件に応じて採用の有無を判断して下さい。

実用ユースケース別スニペット集

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移行のタイミングで段階的に統一していくのが現実的だと考えます。

Careerバナーconsultingバナー