Building serverless React applications with AWS Lambda enables scalable, cost-efficient architectures. Here’s a comprehensive guide to deploying React apps with Lambda, API Gateway, and related AWS services.
Core Architecture Patterns
1. Frontend Hosting with S3 + CloudFront
# serverless.yml
resources:
Resources:
ReactAppBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${self:custom.appBucketName}
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: index.html
AccessControl: PublicRead
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
- DomainName: ${self:custom.appBucketName}.s3.amazonaws.com
Id: ReactAppOrigin
S3OriginConfig: {}
DefaultCacheBehavior:
TargetOriginId: ReactAppOrigin
ViewerProtocolPolicy: redirect-to-https
DefaultTTL: 3600
ForwardedValues:
QueryString: false
Cookies:
Forward: none
Enabled: true
DefaultRootObject: index.html
CustomErrorResponses:
- ErrorCode: 404
ResponseCode: 200
ResponsePagePath: /index.html
2. Serverless API Backend
// handlers/api.js
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';
const client = new DynamoDBClient({ region: process.env.AWS_REGION });
const docClient = DynamoDBDocumentClient.from(client);
export const handler = async (event) => {
try {
const { id } = event.pathParameters;
const command = new GetCommand({
TableName: process.env.TABLE_NAME,
Key: { id }
});
const { Item } = await docClient.send(command);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify(Item)
};
} catch (error) {
console.error(error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal Server Error' })
};
}
};
Deployment Configurations
3. Serverless Framework Setup
# serverless.yml
service: serverless-react-app
custom:
appBucketName: ${self:service}-${opt:stage, 'dev'}-${aws:region}
webpack:
webpackConfig: ./webpack.config.js
includeModules: true
provider:
name: aws
runtime: nodejs18.x
stage: ${opt:stage, 'dev'}
region: us-east-1
environment:
TABLE_NAME: !Ref DataTable
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
Resource: !GetAtt DataTable.Arn
functions:
api:
handler: handlers/api.handler
events:
- http:
path: /api/{id}
method: get
cors: true
resources:
Resources:
DataTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service}-${self:provider.stage}
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
BillingMode: PAY_PER_REQUEST
4. CI/CD Pipeline with GitHub Actions
name: Serverless Deployment
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm install
- run: npm run build
- name: Deploy frontend to S3
run: aws s3 sync build s3://${AWS_S3_BUCKET} --delete
env:
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Install Serverless
run: npm install -g serverless
- name: Deploy backend
run: sls deploy --stage production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Advanced Patterns
5. Server-Side Rendering (SSR) with Lambda@Edge
// lambda-edge/render.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom/server';
import App from '../src/App';
import template from './template';
export const handler = async (event) => {
const { request } = event.Records[0].cf.request;
const html = renderToString(
<StaticRouter location={request.uri}>
<App />
</StaticRouter>
);
const response = {
status: '200',
statusDescription: 'OK',
headers: {
'content-type': [{ key: 'Content-Type', value: 'text/html' }],
},
body: template(html),
};
return response;
};
6. Authentication with Cognito
# serverless.yml
resources:
Resources:
UserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: ${self:service}-${self:provider.stage}
AutoVerifiedAttributes: [email]
UsernameAttributes: [email]
UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
ClientName: ${self:service}-client-${self:provider.stage}
UserPoolId: !Ref UserPool
GenerateSecret: false
ExplicitAuthFlows:
- ALLOW_USER_PASSWORD_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
IdentityPool:
Type: AWS::Cognito::IdentityPool
Properties:
IdentityPoolName: ${self:service}-${self:provider.stage}
AllowUnauthenticatedIdentities: false
CognitoIdentityProviders:
- ClientId: !Ref UserPoolClient
ProviderName: !GetAtt UserPool.ProviderName
Performance Optimization
7. Lambda Cold Start Mitigation
# serverless.yml
functions:
api:
handler: handlers/api.handler
memorySize: 1024 # Minimum recommended for Node.js
timeout: 10
provisionedConcurrency: 3
events:
- http:
path: /api
method: get
8. Caching Strategies
# serverless.yml
functions:
api:
handler: handlers/api.handler
events:
- http:
path: /api
method: get
caching:
enabled: true
ttlInSeconds: 3600
varyByHeaders:
- Authorization
Monitoring and Logging
9. CloudWatch Alarms
resources:
Resources:
LambdaErrorsAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: ${self:service}-${self:provider.stage}-lambda-errors
AlarmDescription: "Lambda function errors exceeding threshold"
Namespace: "AWS/Lambda"
MetricName: Errors
Dimensions:
- Name: FunctionName
Value: !GetAtt ApiLambdaFunction.Arn
Statistic: Sum
Period: 60
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
10. X-Ray Tracing
provider:
tracing:
apiGateway: true
lambda: true
functions:
api:
handler: handlers/api.handler
tracing: Active
Best Practices Checklist
- Infrastructure as Code
- [ ] Use Serverless Framework or AWS CDK
- [ ] Version control all configurations
- [ ] Parameterize environment-specific values
- Security
- [ ] Implement least privilege IAM roles
- [ ] Encrypt sensitive data
- [ ] Use Cognito for authentication
- Performance
- [ ] Optimize Lambda memory size
- [ ] Implement caching layers
- [ ] Use provisioned concurrency for critical paths
- Monitoring
- [ ] Set up CloudWatch alarms
- [ ] Enable X-Ray tracing
- [ ] Log structured JSON data
- CI/CD
- [ ] Automate frontend and backend deployments
- [ ] Implement deployment stages
- [ ] Include performance testing
- Cost Optimization
- [ ] Monitor Lambda execution costs
- [ ] Use appropriate DynamoDB capacity modes
- [ ] Implement auto-scaling where needed
- Error Handling
- [ ] Implement proper Lambda error responses
- [ ] Set up dead letter queues
- [ ] Monitor failed invocations
Complete Example Project Structure
serverless-react-app/
├── src/
│ ├── App.js
│ ├── index.js
│ └── components/
├── handlers/
│ ├── api.js
│ └── auth.js
├── lambda-edge/
│ └── render.js
├── webpack.config.js
├── serverless.yml
├── package.json
└── template.html