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 を使った方法を紹介します。
アプリケーションのビルド
はじめにアプリケーションのビルドです。これは build コマンドでビルドするだけ
ではなく、React Router はデフォルトだとサーバー側の依存ライブラリをバンドルしないため noExternal: true を設定してバンドルされるようにする必要があります。
開発サーバーなどに影響しないようにビルド時以外は設定しないようにしておきます。
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 をバンドルします。
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,
},
});
{
"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 のエントリポイント
作成するためのスクリプトはこちらです。
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)を使ってアプリを実行します。
#!/bin/bash
PORT=8080 ./node_modules/@react-router/serve/bin.js ./build/server/index.js
zip 化
デプロイパッケージの作成 で作成したパッケージを zip にします。
これでアプリケーション側でやることは以上です!
{
"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
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-adapter や react-router-hono-serverあたりを使ってやりたかったのですが、なにぶん上手くいかなかったためボツになりました...😅)
今回のサンプルコードのリポジトリはこちら
Discussion