npm package: @ballatech/react-router7-preset-aws
Description: Minimal AWS Lambda preset for React Router v7. Inspired by the Architect adapter, but without pulling in Architect or its unrelated dependencies that can clash when using AWS CDK with CloudFront, S3, and Lambda to deploy a website.
Latest version: 1.0.1

# @ballatech/react-router7-preset-aws

Minimal AWS Lambda preset for React Router v7. Inspired by the Architect adapter, but without pulling in Architect or its unrelated dependencies that can clash when using AWS CDK with CloudFront, S3, and Lambda to deploy a website.

---

## Why?

- **Architect adapter inspiration**: This preset mirrors the core request/response bridging needed to run React Router v7 on AWS Lambda (headers, cookies, binary body handling) and borrows the binary MIME list approach from Architect.
- **No unrelated deps**: The Architect ecosystem can pull in extra dependencies. When pairing with **AWS CDK + CloudFront + S3 + Lambda**, those can cause version or bundling conflicts. This preset stays tiny and focused.

---

## Features

- **React Router v7 on AWS Lambda (API Gateway v2)**
- **Zero runtime dependencies** beyond React Router node utilities
- **Binary response support** (images, fonts, archives, etc.)
- **Set-Cookie passthrough** compatible with API Gateway
- **Typed** (TypeScript) API with a small surface area

---

## Installation

```bash
pnpm add @ballatech/react-router7-preset-aws react-router @react-router/node
# or
npm i @ballatech/react-router7-preset-aws react-router @react-router/node
# or
yarn add @ballatech/react-router7-preset-aws react-router @react-router/node
```

Peer requirements:

- **react-router**: ^7
- **@react-router/node**: ^7

---

## Usage

Create a Lambda handler that wires your React Router server build to API Gateway v2 events.

```ts
// server.ts
import { createRequestHandler } from '@ballatech/react-router7-preset-aws'
import type { ServerBuild } from 'react-router'

// Generated by `react-router build`
import * as build from './build/server'

export const handler = createRequestHandler({
  build: build as unknown as ServerBuild,
  // Optional: expose platform-specific values to loaders/actions
  // getLoadContext: async (event) => ({
  //   viewerCountry: event.headers['cloudfront-viewer-country'],
  // }),
})
```

Notes:

- The handler expects API Gateway v2 events (HTTP API). It respects `x-forwarded-host` and `host` headers to construct the URL, which works behind CloudFront → API Gateway → Lambda.
- Binary responses are automatically base64-encoded for content types in the common binary list.
- Multiple `Set-Cookie` headers are moved to the API Gateway `cookies` array for proper delivery.

---

## Build

Bundle the handler for Lambda after running the React Router build.

```bash
# Build the React Router app
react-router build

# Bundle the Lambda entry using esbuild (example)
node build.cjs
```

Example `build.cjs` used by the examples in this repo:

```js
const esbuild = require('esbuild')

esbuild
  .build({
    entryPoints: ['server.ts'],
    bundle: true,
    platform: 'node',
    target: 'node22',
    external: ['node:stream'],
    outfile: 'build/lambda/index.cjs',
    sourcemap: true,
    format: 'cjs',
    metafile: true,
    legalComments: 'none',
  })
  .catch((error) => {
    console.error('Build failed:', error)
    throw error
  })
```

---

## AWS CDK (example sketch)

This mirrors the working example in `examples/my-react-router-app-infra`. It hooks the Lambda to an HTTP API and fronts it with CloudFront. Static assets are served from S3 with optimized caching.

```ts
import * as path from 'node:path'
import { Stack, Duration } from 'aws-cdk-lib'
import * as lambda from 'aws-cdk-lib/aws-lambda'
import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs'
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2'
import * as integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations'
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'
import * as s3 from 'aws-cdk-lib/aws-s3'
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment'

export class WebStack extends Stack {
  constructor(scope: any, id: string) {
    super(scope, id)

    const bucket = new s3.Bucket(this, 'ClientBucket', {
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
    })

    const webFn = new lambdaNodejs.NodejsFunction(this, 'WebFn', {
      runtime: lambda.Runtime.NODEJS_22_X,
      memorySize: 1536,
      timeout: Duration.minutes(5),
      entry: path.resolve(__dirname, '../build/lambda/index.cjs'),
      bundling: {
        minify: true,
        target: 'node22',
        mainFields: ['module', 'main'],
        externalModules: ['aws-sdk', '@aws-sdk/*'],
      },
    })

    const api = new apigwv2.HttpApi(this, 'WebApi', {
      defaultIntegration: new integrations.HttpLambdaIntegration('WebIntegration', webFn),
    })

    const distribution = new cloudfront.Distribution(this, 'Distribution', {
      defaultBehavior: {
        origin: new origins.HttpOrigin(
          `${api.httpApiId}.execute-api.${Stack.of(this).region}.${Stack.of(this).urlSuffix}`,
        ),
        allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
        cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
      },
      additionalBehaviors: {
        'assets/*': {
          origin: new origins.S3Origin(bucket),
          allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
          cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
          viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        },
      },
    })

    new s3deploy.BucketDeployment(this, 'DeployClient', {
      destinationBucket: bucket,
      sources: [s3deploy.Source.asset(path.resolve(__dirname, '../build/client'))],
      distribution,
    })
  }
}
```

CloudFront + S3 notes:

- **Static assets**: upload your app's static assets to S3 and front them with CloudFront.
- **Dynamic requests**: add a behavior in CloudFront that forwards SSR/app routes to the API Gateway origin. Ensure headers/cookies (`Host`, `X-Forwarded-Host`, etc.) are forwarded as needed by your setup.

---

## API

```ts
type GetLoadContextFunction = (event: APIGatewayProxyEventV2) => AppLoadContext | Promise<AppLoadContext>

createRequestHandler({
  build: ServerBuild,
  getLoadContext?: GetLoadContextFunction,
  mode?: string, // defaults to process.env.NODE_ENV
}): APIGatewayProxyHandlerV2
```

---

## License

MIT © Ballatech