📦

React Router v7 を Lambda Web Adapter で動かしてみる

に公開

この記事は ESM Advent Calendar 2025 の 17 日目の記事です。


React Router v7 を AWS 上で動かすにはいろいろな選択肢があると思います。
その中で、構築の手軽さと Framework モードの真髄の SSR を両立できる Lambda で動かす方法を説明します。

前提

今回は、手軽さと起動速度を重視し zip 方式を採用します。

アプリケーションはcreate-react-router のテンプレートに SSR も確認したいため loader を追加したものを動かしてみます。

import type { Route } from "./+types/home";
import { Welcome } from "../welcome/welcome";

export async function loader() {
  return "Welcome to React Router";
}

export default function Home({ loaderData }: Route.ComponentProps) {
  return (
    <>
      <p>{loaderData}</p>
      <Welcome />
    </>
  );
}

Lambda Web Adapter

Lambda Web Adapter を使った方法を紹介します。

https://github.com/awslabs/aws-lambda-web-adapter

アプリケーションのビルド

はじめにアプリケーションのビルドです。これは build コマンドでビルドするだけ
ではなく、React Router はデフォルトだとサーバー側の依存ライブラリをバンドルしないため noExternal: true を設定してバンドルされるようにする必要があります。
開発サーバーなどに影響しないようにビルド時以外は設定しないようにしておきます。

vite.config.ts
export default defineConfig(({ command }) => {
  const isBuild = command === "build";
  return {
    ...
    ssr: {
      noExternal: isBuild ? true : undefined,
    }
  };
});

react-router-serve を用意

次に、React Router アプリを動かすための react-router-serveです。node_modules を含めて npm run start すればいいのですが、zip 方式でサイズ上限が厳しいためバンドルしてデプロイ対象に含めていきます。
react-router-serve ./build/server/index.js のように実行できるようになることを目指し、@react-router/serve の CLI をバンドルします。

vite.config.rrs.ts
export default defineConfig({
  build: {
    // 一時的な出力ディレクトリ
    outDir: "./dist/@react-router/serve/dist",
    copyPublicDir: false,
    rollupOptions: {
      // @react-router/serve の CLI エントリポイントを指定
      input: "./node_modules/@react-router/serve/dist/cli.js",
      output: {
        format: "cjs",
        entryFileNames: "[name].js",
      },
    },
    ssr: true,
  },
  ssr: {
    noExternal: true,
  },
});

package.json
{
  "scripts": {
    "build:app": "react-router build",
+   "build:rrs": "vite build -c vite.config.rrs.ts",
    ...
  }
  ...
}

デプロイパッケージの作成

Lambda にデプロイするパッケージを以下の構成で作成します。

_deploy/
├── build/
│   ├── server/         # サーバー側アセット
│   └── client/         # クライアント側アセット
├── node_modules/
│   ├── @react-router/  # バンドル済みの react-router-serve
│   └── bin.js          # react-router-serve のエントリ
└── run.sh              # Lambda Web Adapter のエントリポイント

作成するためのスクリプトはこちらです。

create-package.js
import fs from "node:fs/promises";
import path from "node:path";

async function main() {
  const baseDir = path.resolve(import.meta.dirname, "..");
  const deployDir = path.resolve(baseDir, "_deploy");

  // _deployフォルダ作成
  await fs.rm(deployDir, { recursive: true, force: true });
  await fs.mkdir(deployDir, { recursive: true });

  // buildをコピー
  await fs.cp(`${baseDir}/build`, `${deployDir}/build`, { recursive: true });

  // @react-routerをコピー
  await fs.cp(`${baseDir}/dist`, `${deployDir}/node_modules`, {
    recursive: true,
  });

  // node_modulesから@react-router/serve/bin.jsをコピー
  await fs.cp(
    `${baseDir}/node_modules/@react-router/serve/bin.js`,
    `${deployDir}/node_modules/@react-router/serve/bin.js`
  );

  // run.shをコピー
  await fs.cp(`${baseDir}/run.sh`, `${deployDir}/run.sh`);
  await fs.chmod(`${deployDir}/run.sh`, "0755");
}

main();

しれっと出てきた run.sh は Lambda が実行される際のエントリポイントになります。
ここで react-router-serve(の bin.js)を使ってアプリを実行します。

run.sh
#!/bin/bash
PORT=8080 ./node_modules/@react-router/serve/bin.js ./build/server/index.js

zip 化

デプロイパッケージの作成 で作成したパッケージを zip にします。
これでアプリケーション側でやることは以上です!

package.json
{
  "scripts": {
    "build:app": "react-router build",
    "build:rrs": "vite build -c vite.config.rrs.ts",
    "build:pkg": "node scripts/create-package.js",
+   "build:zip": "cd _deploy && zip -ry ../app.zip ./",
    ...
  }
  ...
}

インフラ

あとは最後に 公式の READMEに沿って以下のインフラ設定をして完了です。

  • 先ほど用意した zip ファイル
  • エントリポイントのシェルスクリプト
  • Lambda Web Adapter レイヤー
  • 環境変数 AWS_LAMBDA_EXEC_WRAPPER
main.tf
resource "aws_lambda_function" "rr_sample_app" {
  ...
  filename         = "./app.zip"
  source_code_hash = filebase64sha256("./app.zip")
  handler = "run.sh"
  layers  = ["arn:aws:lambda:ap-northeast-1:753240598075:layer:LambdaAdapterLayerArm64:24"]
  environment {
    variables = {
      AWS_LAMBDA_EXEC_WRAPPER = "/opt/bootstrap"
    }
  }
}

Lambda で動いている React Router にアクセスできました!しっかり loader も動いています 🎉

さいごに

Lambda Web Adapter で React Router v7 の SSR を手軽に動かしてみました。
他にもっといい方法があったら教えていただきたいです!

(個人的には hono-react-router-adapterreact-router-hono-serverあたりを使ってやりたかったのですが、なにぶん上手くいかなかったためボツになりました...😅)

今回のサンプルコードのリポジトリはこちら
https://github.com/harucn/example-react-router-lambda

Discussion