├── .gitignore ├── .babelrc ├── src ├── index.js └── App.js ├── project.json ├── webpack.apex.config.js ├── webpack.config.js ├── package.json ├── functions └── render-react │ └── src │ ├── index.js │ └── render.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { BrowserRouter } from 'react-router' 4 | import App from './App' 5 | 6 | render( 7 | ( 8 | 9 | 10 | 11 | ), 12 | document.getElementById("app") 13 | ) 14 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-apig-lambda", 3 | "description": "Rendering React with AWS API Gateway and Lambda", 4 | "runtime": "nodejs4.3", 5 | "handler": "lib.default", 6 | "hooks": { 7 | "build": "../../node_modules/.bin/webpack --config ../../webpack.apex.config.js", 8 | "clean": "rm -fr lib" 9 | }, 10 | "memory": 128, 11 | "timeout": 5, 12 | "role": "arn:aws:iam::549001227100:role/lambda_basic_execution" 13 | } 14 | -------------------------------------------------------------------------------- /webpack.apex.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | // NOTE: paths are relative to each functions folder 3 | 4 | module.exports = { 5 | entry: './src/index.js', 6 | target: 'node', 7 | output: { 8 | path: './lib', 9 | filename: 'index.js', 10 | libraryTarget: 'commonjs2' 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | loader: 'babel', 17 | exclude: [/node_modules/] 18 | } 19 | ] 20 | }, 21 | plugins: [ 22 | new webpack.optimize.OccurenceOrderPlugin(), 23 | new webpack.DefinePlugin({ 24 | 'process.env': { 25 | 'NODE_ENV': JSON.stringify('production') 26 | } 27 | }), 28 | new webpack.optimize.UglifyJsPlugin({ 29 | compressor: { 30 | warnings: false 31 | } 32 | }) 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | './src/index' 8 | ], 9 | output: { 10 | path: path.join(__dirname, 'dist'), 11 | filename: 'bundle.js', 12 | publicPath: '/assets/' 13 | }, 14 | plugins: [ 15 | new webpack.optimize.OccurenceOrderPlugin(), 16 | new webpack.DefinePlugin({ 17 | 'process.env': { 18 | 'NODE_ENV': JSON.stringify('production') 19 | } 20 | }), 21 | new webpack.optimize.UglifyJsPlugin({ 22 | compressor: { 23 | warnings: false 24 | } 25 | }) 26 | ], 27 | module: { 28 | loaders: [ 29 | { 30 | test: /\.js$/, 31 | loader: 'babel', 32 | include: path.join(__dirname, 'src') 33 | } 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-apig-lambda", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "NODE_ENV=production webpack --config webpack.config.js", 8 | "deploy:lambda": "apex deploy", 9 | "deploy:s3": "aws s3 cp ./dist/ s3://test.es6.fi/assets/ --recursive", 10 | "deploy": "npm run deploy:lambda && npm run deploy:s3" 11 | }, 12 | "author": "Lari Hoppula", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "babel-core": "^6.14.0", 16 | "babel-loader": "^6.2.5", 17 | "babel-preset-es2015": "^6.14.0", 18 | "babel-preset-react": "^6.11.1", 19 | "babel-preset-stage-2": "^6.13.0", 20 | "history": "^4.2.0", 21 | "react": "^15.3.2", 22 | "react-dom": "^15.3.2", 23 | "react-router": "4.0.0-alpha.3", 24 | "webpack": "^1.13.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /functions/render-react/src/index.js: -------------------------------------------------------------------------------- 1 | import render from './render' 2 | 3 | export default function handle(event, ctx) { 4 | const { 5 | statusCode = 200, 6 | headers = {}, 7 | body = "" 8 | } = render(event.path.replace("/index", "")) 9 | 10 | ctx.succeed({ 11 | statusCode: statusCode, 12 | headers: { 13 | "Content-Type": "text/html", 14 | "Cache-Control": "public, max-age=60", 15 | ...headers 16 | }, 17 | body: ` 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | React API Gateway + Lambda test 26 | 27 | 28 | 29 | 30 |
${body}
31 | 32 | 33 | 34 | 35 | ` 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Match from 'react-router/Match' 3 | import Miss from 'react-router/Miss' 4 | import Link from 'react-router/Link' 5 | 6 | export default () => ( 7 |
8 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 |
21 | ) 22 | 23 | const Home = () => ( 24 |
25 |

Home

26 |
27 | ) 28 | 29 | const About = () => ( 30 |
31 |

About

32 |
33 | ) 34 | 35 | const Topics = ({ pathname }) => ( 36 |
37 |

Topics

38 | 43 | 44 | 45 | ( 46 |

Please select a topic

47 | )}/> 48 |
49 | ) 50 | 51 | const Topic = ({ params }) => ( 52 |
53 |

{params.topicId}

54 |
55 | ) 56 | 57 | const NoMatch = ({ location }) => ( 58 |
Nothing matched {location.pathname}.
59 | ) 60 | -------------------------------------------------------------------------------- /functions/render-react/src/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { renderToString } from 'react-dom/server' 3 | import { ServerRouter, createServerRenderContext } from 'react-router' 4 | import App from '../../../src/App' 5 | 6 | export default function render(path) { 7 | // first create a context for , it's where we keep the 8 | // results of rendering for the second pass if necessary 9 | const context = createServerRenderContext() 10 | 11 | // render the first time 12 | let markup = renderToString( 13 | 17 | 18 | 19 | ) 20 | 21 | // get the result 22 | const result = context.getResult() 23 | 24 | // the result will tell you if it redirected, if so, we ignore 25 | // the markup and send a proper redirect. 26 | if (result.redirect) { 27 | return { 28 | statusCode: 301, 29 | headers: { 30 | Location: result.redirect.pathname 31 | } 32 | } 33 | } else { 34 | let statusCode = 200 35 | // the result will tell you if there were any misses, if so 36 | // we can send a 404 and then do a second render pass with 37 | // the context to clue the components into rendering 38 | // this time (on the client they know from componentDidMount) 39 | if (result.missed) { 40 | statusCode = 404 41 | markup = renderToString( 42 | 46 | 47 | 48 | ) 49 | } 50 | return { 51 | statusCode: statusCode, 52 | body: markup 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-apig-lambda 2 | 3 | > Render React.js on-demand with CDN caching. 4 | 5 | > Minimal example on how to render React & React Router v4 with Amazon API Gateway, AWS Lambda and CloudFront. 6 | 7 | ## Online demo 8 | [https://test.es6.fi](https://test.es6.fi) 9 | 10 | Basic example app from [React Router documentation](https://react-router.now.sh/basic). Initial server-side render and acts as SPA from there. 11 | 12 | ## Dependencies 13 | 14 | * [AWS CLI](https://aws.amazon.com/cli/) for S3 deployment 15 | * [Apex](https://github.com/apex/apex) for Lambda deployment 16 | 17 | ## Deploying to AWS 18 | 19 | 1) Edit `project.json` and set proper lamdba execution `role`. 20 | 21 | 2) Replace `s3://test.es6.fi/assets/` in `package.json` with your S3 bucket, e.g. `s3://your-bucket/assets/`. 22 | 23 | 3) `npm run build` to build front-end code 24 | 25 | 4) `npm run deploy` to deploy lambda and upload front build to S3 26 | 27 | ## Setting up API Gateway 28 | 29 | 1) In API Gateway home, click `Create API` 30 | 31 | 2) Choose `New API` and enter some `API name`, click `Create API`. 32 | 33 | 2) Choose `Actions -> Create resource` 34 | 35 | 3) Check `Configure as proxy resource` and click `Create resource` 36 | 37 | 4) In `/{proxy+} - ANY - Setup`, choose `Integration type` as `Lambda Function Proxy`, select your lambda's AWS region and enter name of your uploaded lambda function (`react-apig-lambda_render-react` if you didn't change name in `project.json`). Click `Save`. 38 | 39 | 5) Choose `Actions -> Deploy API`, set `Deployment stage` as `[New Stage]`, enter stage name and click `Deploy` 40 | 41 | 6) Now you should be able to invoke the lambda renderer by navigating to `https://your-invoke-url/your-stage-name/index` 42 | 43 | ## Setting up CloudFront 44 | 45 | 1) Create distribution, paste your API Gateway url as `Origin domain name`, e.g. `https://your-invoke-url/your-stage-name/index`. Make sure to include `/index`. 46 | 47 | 2) Set your custom domain in `Alternate Domain Names 48 | (CNAMEs)` 49 | 50 | 3) You can leave other settings as they are if you don't want to customize anything, click `Create distribution`. 51 | 52 | 4) Go to your distribution, navigate to `Origins`, click `Create origin` 53 | 54 | 5) Choose your S3 bucket (you should create it now if you haven't already. Make sure there's `assets` directory). Click `Create`. 55 | 56 | 6) Go to your distribution, navigate to `Behaviors`, click `Create Behavior`. 57 | 58 | 7) Set `Path Pattern` as `assets/*`, choose your S3 origin and click `Create`. 59 | 60 | 8) In your domain's DNS management interface, point your domain's `CNAME` to your CloudFront distribution. 61 | --------------------------------------------------------------------------------