API Gateway と Lambda のサーバーレス構成で Express を動かしてみた

API Gateway と Lambda のサーバーレス構成で Express を動かしてみた

2025.12.31

製造ビジネステクノロジー部の小林です。

最近、Express を Lambda で動かす機会がありました。「どうやって Express が Lambda で動くのか?」という仕組みが気になったので、理解を深めるために、公式ドキュメンを見ながら Express を実装してみました。

Express とは?

https://expressjs.com/ja/
公式ドキュメントでは "Minimal and flexible"(最小限で柔軟) と紹介されており、具体的には以下のような機能・特徴を持っています。

  • 軽量で柔軟 - 必要な機能だけを組み込める
  • ミドルウェア - リクエスト/レスポンスの処理を簡単に追加
  • ルーティング - URL パターンに応じた処理を定義
  • 豊富なエコシステム - 数千のミドルウェアパッケージが利用可能

なぜ Express を Lambda で動かすのか?

サーバーレス環境である Lambda に、あえてウェブフレームワークの Express を持ち込むメリットはどこにあるのでしょうか。主な理由は以下の 4 点です。

  1. 既存の Express の知識がそのまま使える
  2. サーバー管理が不要
  3. トラフィックに応じた Auto Scaling が自動的に行われる
  4. 従量課金制のため、常時起動のサーバーに比べてコスト効率が良い

実装してみる

それでは、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() を使ってサーバーを起動します。
https://expressjs.com/ja/starter/hello-world.html

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 つを肩代わりします。

  1. 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(...) が動くようになります。

  1. 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 のミドルウェアを理解する

https://expressjs.com/ja/guide/using-middleware.html

ミドルウェア関数は、リクエストオブジェクト (req)、レスポンスオブジェクト (res)、そして次のミドルウェア関数 (next) にアクセスできる関数です。

ミドルウェアは以下のことができます。

  • 任意のコードを実行
  • req と res オブジェクトを変更
  • リクエスト/レスポンスサイクルを終了
  • 次のミドルウェアを呼び出す (next() を使用)

express.json()

https://expressjs.com/ja/api.html#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 メソッドで」リクエストが来たら、「どの処理(ハンドラー)を実行するか」を結びつける仕組みです。
https://expressjs.com/ja/guide/routing.html

Express では基本的に次の 3 点でルートが決まります。

  1. HTTP メソッド(GET / POST / PUT / DELETE など)
  2. パス(URL)(/api/users など)
  3. ハンドラー関数((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 を学習されている方の参考になれば幸いです。

この記事をシェアする

FacebookHatena blogX

関連記事