Lambda × Express でサーバーレスアプリを動かす構成を図にまとめてみた

Lambda × Express でサーバーレスアプリを動かす構成を図にまとめてみた

2026.05.09

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

最近 TypeScript のドキュメントを読んでいて、ふと疑問が浮かびました。

「TypeScript って結局どこで動くの?」

「Node.js 上で TypeScript × Express を動かすときって、どういうフローになるんだろう?」

この記事では、Lambda 上で TypeScript / Express アプリを動かすときのスタック全体を、レイヤー構造で整理しました。Axios・Zod・Winston・Jest といった定番 npm ライブラリの立ち位置もあわせて図解しています。

全体構成図

構成を図解するとこのような入れ子構造になります。

スクリーンショット 2026-05-09 4.00.56

それぞれの役割を順番に見ていきます。

HTTP リクエスト / イベント

Lambda は単体では動きません。「何かが起きたら実行する」というトリガーが必要です。
代表的なトリガーは以下の 4 つです。

トリガー ユースケース
API Gateway REST / HTTP API のエンドポイント
ALB ALB からの転送
EventBridge スケジュール実行・他 AWS サービス連携
SQS キューからの非同期処理

AWS Lambda(実行環境)

AWS Lambda は、サーバーを自分で管理せずにコードを実行できるサービスです。

通常、アプリケーションを動かすにはサーバーの構築・OS のアップデート・スケーリングといった運用作業が必要です。Lambda を使えば、これらを AWS に任せて「コードの実行」だけに集中できます。リクエストが来たときだけ起動し、使った分だけ課金される従量課金モデルも特長です。

ランタイムとして Node.js を選択することで、JavaScript(TypeScript からコンパイルした JavaScript)をそのまま Lambda 上で実行できます。

https://aws.amazon.com/lambda/
https://docs.aws.amazon.com/lambda/latest/dg/lambda-typescript.html

Node.js(ランタイム)

Node.js は、JavaScript をサーバーサイドで実行するためのランタイム環境です。

もともと JavaScript はブラウザの中でしか動かない言語でしたが、Node.js の登場により、サーバー上でも JavaScript を実行できるようになりました。内部には Google Chrome と同じ V8 エンジンが搭載されており、高速に JavaScript を処理します。

また、非同期 I/O モデルを採用しているため、ファイルの読み書きや API 呼び出しなどの待ち時間が発生する処理を効率よくさばけるのが特長です。

Lambda との関係でいうと、Lambda の実行環境そのものが Node.js ランタイムです。つまり、Lambda 関数は「AWS が用意した Node.js 環境の上で動くアプリケーション」と考えることができます。

Lambda 関数が呼び出される

AWS Node.js ランタイムを起動

コンパイル済みの JavaScript Node.js が実行

結果を返す

https://nodejs.org/

アプリケーションコード

TypeScript

TypeScript は、JavaScript に「型」を追加した言語です。Microsoft が開発しており、JavaScript の上位互換(スーパーセット)として位置づけられています。

つまり、JavaScript のコードはそのまま TypeScript としても動きますが、さらに「この変数は文字列」「この関数は数値を返す」といった型情報を書き添えることができます。

  • 実行前にバグを発見
    • 型の不一致をコンパイル時に検出してくれるため、動かす前にミスに気づけます
  • tsconfig.json で設定を管理
    • コンパイルの対象ファイルや出力先などを 1 つの設定ファイルにまとめます
  • JavaScript への変換が必要
    • Lambda(Node.js)は TypeScript を直接実行できないため、.ts ファイルを tsc や esbuild で JavaScript に変換してからデプロイします。

TypeScript は「開発時の安全性を高める仕組み」であり、最終的に動くのはあくまで変換後の JavaScript です。

https://www.typescriptlang.org/
https://docs.aws.amazon.com/lambda/latest/dg/lambda-typescript.html

JavaScript(コンパイル後)

TypeScript はあくまで「開発時の言語」であり、Lambda が実際に実行するのはコンパイル後の JavaScript です。dist/ ディレクトリに出力された .js ファイルを Lambda にデプロイします。

Express

Express は Node.js 向けの軽量 Web フレームワークです。「この URL にリクエストが来たら、この処理を実行する」というルーティングを簡単に定義できます。

主な役割は以下の 3 つです。

  • ルーティング定義
    • GET /users、POST /items など、URL と HTTP メソッドに応じた処理の振り分け
  • ミドルウェア
    • リクエストが処理に届く前に、認証チェック・ログ記録・入力値検証などを挟み込む仕組み
  • HTTP ハンドリング
    • req(リクエスト)/ res(レスポンス)オブジェクトを使って、受け取ったデータの読み取りや返却を行う

Lambda で Express を動かすには、@codegenie/serverless-express というライブラリでラップするのが定番です。Lambda が受け取るイベントを Express が理解できるリクエスト形式に変換してくれます。

import express from "express";
import serverlessExpress from "@codegenie/serverless-express";

const app = express();

app.get("/hello", (req, res) => {
  res.json({ message: "Hello from Lambda!" });
});

export const handler = serverlessExpress({ app });

上の例では、/hello に GET リクエストが来たら JSON メッセージを返す、というシンプルな API を定義しています。最後の行で Express アプリを Lambda のハンドラーとしてエクスポートしています。

別のアプローチとして、serverless-http というライブラリもあります。こちらは Express 以外(Koa・Hapi 等)にも対応した汎用ラッパーです。

https://expressjs.com/
https://github.com/CodeGenieApp/serverless-express

npm ライブラリ群

ここからは「アプリコードの中から使う部品」です。Node.js の node_modules に入り、TypeScript の型定義と合わせて使います。

Axios — HTTP クライアント

Axios は、外部の API にリクエストを送るための HTTP クライアントライブラリです。Lambda の中から別のサービスや API にデータを取りに行きたいときに使います。

レスポンスの自動 JSON 変換やエラーハンドリングが充実しており、シンプルなコードで HTTP 通信を実装できます。

import axios from "axios";

// 外部 API からユーザー一覧を取得
const res = await axios.get("https://api.example.com/users");
console.log(res.data);

上の例では、外部の API に GET リクエストを送り、返ってきたデータを res.data で取得しています。await を使っているので、レスポンスが返ってくるまで待ってから次の処理に進みます。

  • インターセプター機能 — すべてのリクエストに認証ヘッダーを自動付与したり、失敗時にリトライする処理を共通化できます。
  • タイムアウト設定 — 応答が遅い API に対して、一定時間で打ち切る設定も追加できます。

https://axios-http.com/
https://www.npmjs.com/package/axios

Zod — スキーマバリデーション

Zod は TypeScript のスキーマ定義・バリデーションライブラリです。「受け取ったデータが正しい形をしているか」をチェックする仕組みを、シンプルに書けます。

たとえば API にリクエストが来たとき、「ID は UUID 形式か」「名前は空でないか」「メールアドレスの形式は正しいか」といった入力値の検証を行えます。

さらに、スキーマ定義から TypeScript の型を自動で生成してくれるため、バリデーションと型定義を二重に書く手間がなくなります。

import { z } from "zod";

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
});

// スキーマから TypeScript の型を自動生成
type User = z.infer<typeof UserSchema>;

// リクエストボディを検証(不正なデータでもエラーを投げずに結果を返す)
const result = UserSchema.safeParse(requestBody);

if (!result.success) {
  // どの項目がどう間違っているかを取得できる
  console.log(result.error.issues);
}

上の例では、UserSchema で「ユーザーデータはこういう形であるべき」というルールを定義し、safeParse で実際のリクエストボディがそのルールに合っているかを確認しています。

https://zod.dev/

Winston — 構造化ロギング

Winston は Node.js のロギングライブラリです。アプリケーションのログを、見やすく探しやすい形で出力してくれます。

Lambda のログは CloudWatch Logs に流れます。このとき console.log でただ文字列を出すだけだと、あとから特定のリクエストを探すのが大変です。

Winston を使って JSON 形式で出力すれば、「どのユーザーの」「どのパスへのリクエストか」をキーで検索できるようになります。

import winston from "winston";

const logger = winston.createLogger({
  level: "info",
  format: winston.format.json(),
  transports: [new winston.transports.Console()],
});

logger.info("Request received", { userId: "123", path: "/users" });

上の例では、「リクエストを受け取った」というメッセージに加えて、ユーザー ID やパスの情報を JSON のキー付きで記録しています。

  • ログレベルの切り替え — error / warn / info / debug など、重要度に応じて出力を絞れます
  • 複数の出力先に同時送信 — コンソール・ファイル・HTTP など、用途に合わせて出力先(Transport)を追加できます

https://github.com/winstonjs/winston

Jest — テストフレームワーク

Jest は Meta が開発したテストフレームワークです。「コードが期待どおりに動くか」を自動で確認する仕組みを提供してくれます。

Jest の嬉しいポイントは、設定ファイルをほとんど書かなくてもすぐ使い始められることです。また「この API を呼んだふりをする(モック)」機能や「コードのどこがテストされていないか(カバレッジ)」を調べる機能も最初から入っています。

import { handler } from "../src/index";

test("GET /hello returns 200", async () => {
  const event = { httpMethod: "GET", path: "/hello" };
  const result = await handler(event, {} as any);
  expect(result.statusCode).toBe(200);
});

上の例では、Lambda のハンドラーに「GET /hello」というリクエストを渡し、返ってきたステータスコードが 200(正常)であることを確認しています。
TypeScript で書いたコードをテストするには ts-jest を追加します。

# ts-jest のセットアップ
npm install --save-dev jest ts-jest @types/jest
npx ts-jest config:init

https://jestjs.io/

GitHub Actions による自動デプロイ

コードを書いたら、Lambda に届けるまでの作業も自動化できます。手動で毎回コンパイル → テスト → デプロイするのは手間がかかりますし、ミスも起きやすいためです。

実際の .yml はざっくりこのような構成になります。

name: Deploy to Lambda

on:
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      # リポジトリのコードを取得
      - uses: actions/checkout@v6

      # Node.js のセットアップ
      - uses: actions/setup-node@v6
        with:
          node-version: 24

      # 依存パッケージをインストール
      - run: npm ci

      # テスト実行
      - run: npm test

      # CDK で Lambda にデプロイ(ビルド → ZIP化 → 更新まで一括)
      - run: npx cdk deploy --require-approval never
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ap-northeast-1

まとめ

今回は Lambda 上で TypeScript / Express アプリを動かす際のフローと、各レイヤーが何を担っているかを整理してみました。

図解にすることで、全体像がクリアになりました!
この記事がどなたかの参考になれば幸いです。

この記事をシェアする

関連記事