AWS Lambda¶
AWS Lambda Hono Validation esbuild
Hono で動作し esbuild で単一のデプロイアーティファクトにバンドルされる AWS Lambda 関数。同梱のアプリは最初に必要となる 2 つのパターン — JSON ボディの POST と パスパラメータの GET — を示しており、いずれも API Gateway に適合する hono/aws-lambda アダプター経由で配線されている。HTTP API トリガーと組み合わせれば、ウィザードを離れることなく 動作するエンドポイントが得られる。
ビルドパイプラインは意図的に単発である: pnpm build は ReScript をコンパイルし、続いて src/Server.res.mjs を dist/index.mjs (ESM、Node ターゲット) に esbuild する。zip にしてアップロードしてもよいし、SAM / CDK / Terraform に組み込んでもよい — アーティファクトは同じである。
生成内容¶
my-project/
├── rescript.json
├── package.json
├── src/
│ ├── Server.res # Hono app, exposes `let handler = HonoLambda.handle(app)`
│ ├── HonoLambda.res # bindings over hono/aws-lambda's `handle`
│ ├── Hono.res # Hono bindings
│ ├── Validation.res # zod or sury — selected in the wizard
│ └── __tests__/Server.test.mjs # vitest smoke import
├── README.md # API / Deploy / Bundling Strategy / DynamoDB Recipe
├── LICENSE # MIT, holder = project name
├── .nvmrc # Node 24 (matches the Lambda runtime)
├── .gitignore # node_modules, dist/, *.zip, .aws-sam/
├── .editorconfig # 2-space indent, LF line endings
└── .github/
├── dependabot.yml # weekly npm updates
└── workflows/ci.yml # install + rescript build + bundle + vitest
ウィザードオプション¶
オプション |
効果 |
|---|---|
Project name |
npm の |
Package manager |
npm / pnpm / yarn / bun。 |
Validation library |
|
主要な依存¶
パッケージ |
用途 |
バージョン |
|---|---|---|
|
ReScript コンパイラ |
|
|
標準ライブラリ |
|
|
コンパイル済み |
|
|
|
|
|
リクエストボディのバリデーション (ウィザードで選択) |
|
|
|
|
|
スモークテストランナー |
|
|
|
|
テンプレートは @types/aws-lambda や aws-lambda-ric を事前インストールしない。Hono のアダプター (hono/aws-lambda) が返す関数のシグネチャは API Gateway が期待するハンドラの形に合致するため、同梱のルートには別途のハンドラ型は不要である。APIGatewayProxyEventV2 や Context を扱う手書きハンドラを書く場合は、後から @types/aws-lambda を追加するとよい。
主要なファイル¶
src/Server.res¶
Hono アプリと、AWS Lambda が呼び出し時に解決する名前付き handler エクスポート。トップレベルの let バインディングは自動的に ESM の名前付きエクスポートになるため、let handler = HonoLambda.handle(app) は ReScript における export const handler = ... の等価物である:
type createOrderResponse = {orderId: string, productId: string, quantity: int}
let app = Hono.createApp()
app->Hono.get("/", ctx => ctx->Hono.text("Lambda + Hono + ReScript"))
app->Hono.get("/orders/:id", ctx => {
let id = ctx->Hono.req->Hono.paramAt("id")
ctx->Hono.json({"orderId": id, "status": "pending"})
})
app->Hono.post("/orders", async ctx => {
let raw = await ctx->Hono.req->Hono.jsonBody
switch Validation.parseCreateOrderPayload(raw) {
| Error(msg) => ctx->Hono.status(400)->Hono.json({"error": msg})
| Ok(payload) =>
let response: createOrderResponse = {
orderId: "ord_" ++ Date.now()->Float.toString,
productId: payload.productId,
quantity: payload.quantity,
}
ctx->Hono.status(201)->Hono.json(response)
}
})
let handler = HonoLambda.handle(app)
これを (%%raw ではなく) 実際の let バインディングで行うことで、ReScript コンパイラが HonoLambda の import を出力することが保証される — その上で esbuild が アダプターを dist/index.mjs にバンドルする。
src/HonoLambda.res¶
Hono アダプターに対する 2 行のバインディング — 一目で読み切れる小ささだが、テンプレートが必要とする API Gateway 関連の知識はこれですべてカバーできる:
type lambdaEvent
type lambdaResult
type handler = lambdaEvent => promise<lambdaResult>
@module("hono/aws-lambda") external handle: Hono.app => handler = "handle"
このアダプターは API Gateway からの REST API (v1) と HTTP API (v2) の両方のイベントを受け取り、Hono がルーティングできる fetch 互換のリクエストに変換する。
src/Validation.res¶
両バリアントは parseCreateOrderPayload: JSON.t => result<createOrderPayload, string> を公開する。ルートハンドラは Error で 400 を返し、Ok で処理を続行する — これはウィザードが同梱するすべてのサーバーテンプレートで使われているパターンであり、マッスルメモリが活かせる。より広いエコシステム (zod-to-openapi、hono/zod-openapi) を取りたいなら zod を、ReScript ネイティブの使い心地と小さいランタイムフットプリントを優先するなら sury を選ぶとよい。
src/__tests__/Server.test.mjs¶
モジュールがロードされることを検証する 1 行のスモークテスト:
await expect(import("../Server.res.mjs")).resolves.toBeDefined();
CI ではバンドルステップと並んで実行されるため、壊れたハンドラは早期に失敗する。実際の Lambda 統合テストでは、aws-lambda-mock-context や sam local invoke で合成した API Gateway イベントを使い、バンドル済みの dist/index.mjs を駆動する。
エンドポイント¶
エンドポイント |
メソッド |
説明 |
|---|---|---|
|
GET |
ヘルスチェック ( |
|
GET |
ID (パスパラメータ) でオーダーを検索する |
|
POST |
|
リクエスト/レスポンスの形状¶
POST /orders
Content-Type: application/json
{ "productId": "sku-42", "quantity": 3 }
HTTP/1.1 201 Created
Content-Type: application/json
{ "orderId": "ord_1714142512345", "productId": "sku-42", "quantity": 3 }
GET /orders/ord_1714142512345
HTTP/1.1 200 OK
Content-Type: application/json
{ "orderId": "ord_1714142512345", "status": "pending" }
ボディが欠落または不正な場合は 400 を返す:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{ "error": "Validation failed" }
npm スクリプト¶
スクリプト |
説明 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
build スクリプトは 2 つのステップを && で連結する (パッケージマネージャのプレフィックスは ウィザードの選択から展開される)。アップロードするアーティファクトは dist/index.mjs である。
デプロイ手順¶
ビルドとアップロード¶
pnpm build # rescript && esbuild → dist/index.mjs
cd dist && zip lambda.zip index.mjs
aws lambda update-function-code \
--function-name my-project \
--zip-file fileb://lambda.zip
Set the function's Handler to index.handler and Runtime to Node.js 24. Use API Gateway HTTP API as the trigger unless you need a feature only REST API offers.
バンドルサイズの逃げ道¶
デフォルトのバンドルは、コールドスタートのレイテンシを下げるためにすべての依存をインライン化する。アーティファクトが 50 MB の直接アップロード上限 (展開後 250 MB) に近づいてきたら、以下のいずれかを採用する:
Lambda 提供の依存を external としてマークする¶
AWS SDK v3 (@aws-sdk/*) は Node.js Lambda ランタイムにプリインストールされている。これを除外するとコマンドあたり数 MB 削減できる:
esbuild src/Server.res.mjs --bundle --platform=node \
--outfile=dist/index.mjs --format=esm \
--external:@aws-sdk/* --external:aws-sdk
--external でマークするものは実行時にすでに存在している必要がある — Lambda が同梱しているか、Lambda Layer で公開しているかのいずれかである。
Lambda Layers¶
大きなネイティブ依存 (Sharp、Prisma エンジン、カスタムバイナリ) は、Layer として一度公開し、バンドルから除外する:
aws lambda publish-layer-version \
--layer-name my-project-deps \
--compatible-runtimes nodejs24.x \
--zip-file fileb://layer.zip
aws lambda update-function-configuration \
--function-name my-project \
--layers arn:aws:lambda:<region>:<account>:layer:my-project-deps:1
Layer は実行環境ごとにキャッシュされるため、コールドスタートのコストは呼び出しごとではなく コンテナごとに一度だけ発生する。
Tree-shaking とソースマップ¶
本番ビルドには --tree-shaking=true --minify --sourcemap を追加する。ソースマップはデプロイアーティファクトの外に残る (dist/index.mjs.map) — Lambda の zip を膨らませることなく、シンボリケート済みのスタックトレースのために APM (Datadog、Sentry、CloudWatch RUM) にアップロードできる。
DynamoDB レシピ (簡易版)¶
同梱の README には完全な DynamoDB 統合が記載されている。短縮版は以下の通り:
pnpm add @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
type docClient
@module("@aws-sdk/lib-dynamodb") @new
external makeClient: 'opts => docClient = "DynamoDBDocumentClient"
@send external send: (docClient, 'command) => promise<'result> = "send"
対象テーブルに対して Lambda の IAM ロールに dynamodb:PutItem / dynamodb:GetItem を付与する。@aws-sdk/* をプリインストールする Node ランタイムを使い続けるなら、これらを --external でマークしてアーティファクトをスリムに保つ。
ローカルで試す¶
同梱のルートは、Hono を Node 上で起動し Lambda であるかのように振る舞わせることで、AWS アカウントなしでテスト可能である。使い捨てのエントリファイルを追加する:
// src/Local.res
HonoNodeServer.serve({fetch: Server.app->HonoNodeServer.honoFetch, port: 3000})
node src/Local.res.mjs で起動し、http://localhost:3000 に curl をかける:
curl -X POST http://localhost:3000/orders \
-H 'Content-Type: application/json' \
-d '{"productId":"sku-42","quantity":3}'
# => 201 {"orderId":"ord_…","productId":"sku-42","quantity":3}
curl http://localhost:3000/orders/ord_42
# => 200 {"orderId":"ord_42","status":"pending"}
Lambda に出荷する Hono アダプター (hono/aws-lambda) とローカルで使う Node サーバー (@hono/node-server) はどちらも同じ app インスタンスをラップしている。そのため、両端のランタイムアダプター以外は 挙動が同じである。これはデプロイの往復コストを払う前にルートを反復改善する最も安上がりな方法である。
完全な Lambda 忠実度を得るには AWS SAM を使う:
sam local invoke MyFunction -e events/post-order.json
sam local invoke は本番の Lambda ランタイムと一致するコンテナ内で、バンドル済みの dist/index.mjs を起動する。コールドスタートの挙動、環境変数、IAM のなりすましがすべてクラウドの動作に一致する。
2日目以降のレシピ¶
Hono エンドポイントの追加 —
/ordersと並行して別のルートを追加するTypeScript からの変換 — 既存の Lambda をモジュールごとに ReScript へ移植する
Import の最適化 — バンドルをスリムに保つ (コールドスタートのレイテンシが重要)
プロジェクトを開いた後の ReScript 側のエディタワークフローについては、機能概要 を参照してほしい。
補足¶
初期状態で API Gateway 適合。
hono/aws-lambdaは REST API (v1) と HTTP API (v2) 両方のイベント形状を受け付けるため、関数をどちらのトリガーに配線してもコード変更なしで動作する。HTTP API の方が安く高速である — REST API でしか得られない機能 (mTLS、ゲートウェイでのリクエスト検証など) が必要でない限り、こちらを選ぶ。Single-artifact bundle.
pnpm buildproducesdist/index.mjs— every dependency inlined, ESM, Node target. Zip it (cd dist && zip lambda.zip index.mjs) andaws lambda update-function-codeit. The function's Handler setting isindex.handler; the Runtime is Node.js 24.50 MB のアップロード上限に注意。 デフォルトのバンドルは、コールドスタートのレイテンシを下げるために 全依存をインライン化するが、新しい依存ごとにアーティファクトは大きくなる。README の Bundling Strategy セクションでは 3 つの逃げ道を解説している: Lambda 提供の依存を
--externalでマークする (AWS SDK v3 は Node ランタイムにプリインストール済み — 除外すれば数 MB 削減できる)、大きなネイティブ依存を Lambda Layer として公開する (Sharp、Prisma エンジン)、本番では tree-shaking + minify + sourcemap を有効にする。DynamoDB レシピは README に記載済み。 同梱の DynamoDB Recipe セクションでは、
@aws-sdk/lib-dynamodb(DynamoDBDocumentClient、send) に対する最小限の ReScript バインディングを示し、Lambda の IAM ロールにdynamodb:PutItem/dynamodb:GetItemを付与することを促している。pnpm add @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodbでインストールする — プリインストールされていないランタイムを使っている場合は これらを--externalに しない こと。handlerを実際のletバインディングにしている理由がある。 ReScript コンパイラはモジュールが実際に参照されたときのみHonoLambdaの import を出力する。let handler = HonoLambda.handle(app)と定義することで import は生き残るが、%%raw("export const handler = ...")に切り替えると、サイレントにバンドルから削除されてしまう。スモークテストはモジュールのロードのみを検証する。
src/__tests__/Server.test.mjsはawait import("../Server.res.mjs")を実行し、それが解決することをアサートする。実際の Lambda 統合テストでは、aws-lambda-mock-contextや SAM のsam local invokeで合成した API Gateway イベントを使い、バンドル済みのdist/index.mjsを駆動する。dist/、*.zip、.aws-sam/は gitignore 済み。 ビルド成果物と SAM CLI の作業状態はリポジトリに入らない。Node 24 is mandatory.
engines.nodeis>=24,.nvmrcsays24, and the README's Deploy section pins the Lambda runtime toNode.js 24. The Layer publish snippet interpolatesnodejs24.xfrom the same source — bumpingTemplateVersions.NODE_MAJORupdates all three locations atomically.reserved concurrency / provisioned concurrency は未設定。 同梱のテンプレートは オンデマンドを前提としている。プロビジョンドコンカレンシーを必要とするほどレイテンシに敏感なワークロードの場合は、Lambda コンソール (または SAM/CDK/Terraform) で設定する — これはバンドルには影響しない。
コールドスタートは重要。 ReScript のコンパイル出力と Hono はどちらも小さい (
honoは minzip で 約 14 KB) ため、コールドスタートは V8 のウォームアップと、モジュールロード時にインスタンス化した AWS SDK クライアントが支配する。SDK クライアントの構築はハンドラのクロージャの外に移し、同一コンテナ内の呼び出しを跨いで温かく保つ。一方、リクエストスコープの状態はハンドラ内に保持する。バンドリングは意図的であり、オプションではない。
node src/Server.res.mjsを直接実行しても Lambda としては動かない — Lambda は名前付き ESM エクスポートを呼び出し、ReScript コンパイラが出力する相対 import はindex.mjsだけが切り出されてしまうと壊れる。常に esbuild の出力をアップロードし、生の.res.mjsファイルをアップロードしてはならない。CI はビルドとテストの両方を実行する。 同梱の
.github/workflows/ci.ymlはpnpm build(ReScript のコンパイルと esbuild のバンドルを実行) を呼び出した後、pnpm testを実行する。バンドルの失敗 (例:@module(...)バインディングの忘れ、依存の欠落) は マージをブロックする — 知りたいタイミングそのものである。