API Gateway と Lambda のサーバーレス構成で Express を動かしてみた
製造ビジネステクノロジー部の小林です。
最近、Express を Lambda で動かす機会がありました。「どうやって Express が Lambda で動くのか?」という仕組みが気になったので、理解を深めるために、公式ドキュメンを見ながら Express を実装してみました。
Express とは?
公式ドキュメントでは "Minimal and flexible"(最小限で柔軟) と紹介されており、具体的には以下のような機能・特徴を持っています。
- 軽量で柔軟 - 必要な機能だけを組み込める
- ミドルウェア - リクエスト/レスポンスの処理を簡単に追加
- ルーティング - URL パターンに応じた処理を定義
- 豊富なエコシステム - 数千のミドルウェアパッケージが利用可能
なぜ Express を Lambda で動かすのか?
サーバーレス環境である Lambda に、あえてウェブフレームワークの Express を持ち込むメリットはどこにあるのでしょうか。主な理由は以下の 4 点です。
- 既存の Express の知識がそのまま使える
- サーバー管理が不要
- トラフィックに応じた Auto Scaling が自動的に行われる
- 従量課金制のため、常時起動のサーバーに比べてコスト効率が良い
実装してみる
それでは、API Gateway と Lambda のサーバーレス構成で Express を実装してみます。インフラは CDK で実装します。
前提
- Node.js: v22
- パッケージマネージャ: 本記事では pnpm を使用しますが、npm や yarn でも代用可能です
必要なパッケージのインストール
# Express 本体(Webフレームワーク)
pnpm install express
# Express の型定義ファイル
pnpm install -D @types/express
# AWS Lambda で Express を動かすためのアダプターライブラリ
# (API Gateway のイベントを Express の req/res に変換する役割)
pnpm install @codegenie/serverless-express
ディレクトリ構成
express-lambda-app/
├── lambda/
│ ├── app.ts # Express アプリ本体(ルーティング定義)
│ ├── handler.ts # Lambda エントリーポイント
│ └── services.ts # ビジネスロジック
├── lib/
│ └── express-lambda-app-stack.ts # CDK スタック定義
└── package.json
- app.ts: Express のロジック(ルーティング、ミドルウェア)
- handler.ts: Lambda との接続(インフラ層)
- services.ts: ビジネスロジック(Express 非依存)
app.ts - Express アプリ本体
Express のルーティングとミドルウェアを定義します。ポートのリッスンは Lambda 環境では不要なため、app.listen() は呼びません。
import express, { Application, Request, Response } from "express";
import { getUsers, createUser } from "./services";
const app: Application = express();
app.use(express.json());
/**
* ユーザー一覧を取得エンドポイント
*/
app.get("/api/users", (req, res) => {
const users = getUsers();
res.json({ users });
});
/**
* ユーザー作成エンドポイント
*/
app.post("/api/users", (req, res) => {
const { name } = req.body;
const user = createUser(name);
res.json({ message: "User created", user });
});
export { app };
なぜ app.listen() が不要なのか?
通常、Express の公式ドキュメントやチュートリアルでは、以下のように app.listen() を使ってサーバーを起動します。
const express = require("express");
const app = express();
const port = 3000;
app.get("/", (req, res) => {
res.send("Hello World!");
});
// HTTPサーバーを起動
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
しかし、Lambda 上で動かす場合、この記述は不要(書いても機能しない)です。なぜなら、アーキテクチャそのものが異なるからです。
サーバーと Lambda の違い
通常の Express アプリは、サーバー上で常駐プロセスとして起動し、指定されたポート(例: 3000 番)でリクエストが来るのをじっと待ち構えます(Listen)。
一方で Lambda はイベント駆動です。 Lambda 自体がリクエストを待ち受けるのではなく、「API Gateway がリクエストを受け取り、Lambda を関数として呼び出す」という流れになります。
つまり、「Listen」は API Gateway に任せているため、Express 側で待機する(app.listen)必要がない、というわけです。
handler.ts - Lambda エントリーポイント
import serverlessExpress from "@codegenie/serverless-express";
import { app } from "./app";
/**
* Express アプリを Lambda 用にラップ
*
* 重要:handler の外で初期化する
* Lambda のウォームスタート時にキャッシュを再利用できる
*/
const handler = serverlessExpress({ app });
export { handler };
handler.ts は 「Lambda から見た入口」です。Express は HTTP サーバー(req/res)前提で動きますが、API Gateway が渡してくるのは HTTP そのものではなく、イベント(JSON) です。
そこで @codegenie/serverless-express が “変換役(アダプター)” になり、次の 2 つを肩代わりします。
- API Gateway のイベントを Express の req 相当に変換
API Gateway → Lambda に来るイベントは例えばこんな形です。
- httpMethod(GET/POST など)
- path(/api/users など)
- headers
- body
serverless-express はこれを受け取って、Express が期待する形に寄せます。
- req.method
- req.url / req.path
- req.headers
- req.body(必要なら JSON パースもここまでで整える)
結果として app.get(...) や app.post(...) が動くようになります。
- Express のレスポンスを Lambda の戻り値に変換
Express 側は、
- res.status(200).json(...)
- res.set(...)
のようにレスポンスを書きます。serverless-express はそれを受け取って、Lambda が API Gateway に返すべき形式である、
- statusCode
- headers
- body(JSON 文字列など)
に変換して返します。
services.ts - ビジネスロジック層
ここでは、Web フレームワーク(Express)やインフラ(Lambda)に依存しない、純粋なアプリケーションのロジックを記述します。
export interface User {
id: number;
name: string;
}
/**
* ユーザー一覧を取得
*/
export const getUsers = (): User[] => {
// 実際はここで DynamoDB や RDS にアクセスする
return [
{ id: 1, name: "山田" },
{ id: 2, name: "田中" },
];
};
/**
* ユーザーを作成
*/
export const createUser = (name: string): User => {
if (!name) {
throw new Error("name is required");
}
// 実際はここで DB に保存
return {
id: 3,
name,
};
};
Express のミドルウェアを理解する
ミドルウェア関数は、リクエストオブジェクト (req)、レスポンスオブジェクト (res)、そして次のミドルウェア関数 (next) にアクセスできる関数です。
ミドルウェアは以下のことができます。
- 任意のコードを実行
- req と res オブジェクトを変更
- リクエスト/レスポンスサイクルを終了
- 次のミドルウェアを呼び出す (next() を使用)
express.json()
JSON ペイロードを使用して受信要求を解析し、body-parser に基づくミドルウェアです。
app.use(express.json());
このミドルウェアは、
- Content-Type: application/json のリクエストを検出
- リクエストボディを JSON としてパース
- パース結果を req.body に格納
express.json() がないと、req.body は undefined になります。
その他のミドルウェア
公式ドキュメントには、他にも便利なミドルウェアが紹介されています。
- express.urlencoded() - HTML フォームのデータをパース
- express.static() - 静的ファイル(CSS、画像など)を配信
- cors() - CORS(クロスオリジン)を有効化
ルーティング
ルーティングは「どの URL に対して」「どの HTTP メソッドで」リクエストが来たら、「どの処理(ハンドラー)を実行するか」を結びつける仕組みです。
Express では基本的に次の 3 点でルートが決まります。
- HTTP メソッド(GET / POST / PUT / DELETE など)
- パス(URL)(/api/users など)
- ハンドラー関数((req, res) => { ... })
同じパスでもメソッドが違えば別の処理になる
同じ /api/users でも、GET と POST は用途が違うため、別のルートとして定義できます。
// GETメソッド
app.get('/api/users', (req, res) => {
res.json({ users: [...] });
});
// POSTメソッド
app.post('/api/users', (req, res) => {
const { name } = req.body;
res.json({ message: 'User created', name });
});
req と res は何?
- req(Request):リクエスト情報(パス、クエリ、ヘッダー、ボディなど)
- 例:req.query(?page=1 など)、req.params(/users/:id など)、req.body
- res(Response):レスポンスを返すためのオブジェクト
- 例:res.status(200), res.json(...), res.send(...)
Express は主要な HTTP メソッドに対応しています。
- app.get() - GET リクエスト
- app.post() - POST リクエスト
- app.put() - PUT リクエスト
- app.delete() - DELETE リクエスト
CDK によるインフラ定義
CDK を使って Lambda と API Gateway を定義します。この CDK スタックは、「Express を動かす Lambda 関数」と、それを HTTP で呼び出すための API Gateway(REST API) を作成します。
import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs";
export class ExpressLambdaAppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Lambda 関数の定義
const expressFunction = new nodejs.NodejsFunction(this, "ExpressFunction", {
entry: "lambda/handler.ts",
handler: "handler",
runtime: lambda.Runtime.NODEJS_22_X,
timeout: cdk.Duration.seconds(10),
memorySize: 128,
});
// API Gateway の定義
new apigateway.LambdaRestApi(this, "ExpressApi", {
handler: expressFunction,
proxy: true,
deployOptions: { stageName: "v1" },
});
}
}
ポイント
- entry: 'lambda/handler.ts'
Lambda のエントリーポイントファイルを指定します。ここで指定するのは Express アプリ本体(app.ts)ではなく handler.ts です。handler.ts が serverless-express を使って Express を呼び出す「Lambda の入口」になるためです。 - handler: 'handler'
entry で指定したファイルの中の export されている関数名です。つまり lambda/handler.ts の export const handler = ... を指します。 - handler: expressFunction
この API のバックエンドに 先ほど作った Lambda を紐づけます。 - proxy: true
どのパスへのリクエストも API Gateway 側では細かく定義せず、まとめて Lambda に渡す設定です。これによりルーティングは Express 側(app.get('/api/users', ...) など)だけで完結します。
以上で実装は完了です。実際に AWS 環境へデプロイして動作を検証します。
動作確認
デプロイが完了すると、API Gateway のエンドポイント URL が作成されます。この URL が Lambda への入り口となります。
GET リクエストの確認
まずはデータを取得する GET リクエストを送信し、ルーティングが正しく機能しているか確認します。
# ユーザー一覧を取得
curl https://XXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/v1/api/users
実行結果: Lambda が起動し、app.ts の app.get('/api/users', ...) が実行されます。
# レスポンス
{
"users": [
{ "id": 1, "name": "山田" },
{ "id": 2, "name": "田中" }
]
}
無事に JSON データが返ってきました。これで、API Gateway から Lambda を経由して Express が応答していることが分かります。
POST リクエストの確認
次に、データを送信する POST リクエストをテストします。ここでは express.json() ミドルウェアが正しくリクエストボディをパースできているかがポイントです。
# ユーザーを作成(1行で実行)
curl -X POST https://XXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/v1/api/users -H "Content-Type: application/json" -d '{"name":"佐藤"}'
実行結果:
{
"message": "User created",
"user": {
"id": 3,
"name": "佐藤"
}
}
送信した {"name": "佐藤"} が正しくパースされ、レスポンスに含まれて返ってきました。これにより、通常の Express アプリと同様に req.body が利用できていることが確認できました。
おわりに
今回は、Express を Lambda で動かす仕組みを理解するために、公式ドキュメントを参照しながら実装してみました。
次回は services.ts を実際のデータストア(DynamoDB や RDS など)につないでデータベースの接続を試してみたいと思います。
この記事が Express を学習されている方の参考になれば幸いです。







