Rendering and Serving a Create-React-App

Rendering and Serving a Create-React-App from an Express Server within a Lambda Function

Yat Badal
March 1, 2023
Blog cover image

Rendering and Serving a Create-React-App from an Express Server within a Lambda Function

Serverless architecture has emerged as one of the hottest solutions in recent times. Its ease-of-use, speed and simplicity make it a clear winner when it comes to rapid application development. The Serverless Framework extends on these core values and provides a solid ecosystem for developing Serverless applications with fantastic support and tooling. With a few lines of YAML markup and Javascript, you’re able to interactively deploy infinitely scalable*, publicly available APIs or services atop AWS’s Lambda Functions.

As a fun, little, proof-of-concept, I thought it would be interesting to see if I could get a Lambda function running an Express.js server to render and serve (Server-Side Render) a non-ejected Create-React-App (CRA) build.

Right, let’s get into it, we need to:

Bootstrap a Serverless application
Setup Express.js
Render and serve a CRA
Prerequisites
Serverless framework — you can find installation instructions here. Be sure to also configure your AWS credentials.
Node 8.10
Bootstrap
Let’s start by creating a new project directory and initializing it with NPM

$ mkdir my-project && cd my-project
$ npm init -f
Create a serverless.yml and index.js file

$ touch serverless.yml index.js
Next, let’s pull in Express.js and serverless-http

$ npm i -S serverless-http express
We also want to install the serverless-webpack plugin which will help use all the shiny new JS. The serverless-offline plugin will allow us to deploy our Serverless stack locally. We will use webpack-node-externals to exclude node_modules from our build.

$ npm i -D webpack serverless-webpack serverless-offline webpack-node-externals
Configure Serverless
Within serverless.yml, place the following YAML:

//serverless.yml
service: my-project
plugins:
 - serverless-webpack
 - serverless-offline
custom:
 webpack:
   webpackConfig: ./webpack.config.js
   includeModules: true
   packager: 'npm'
provider:
 name: aws
 runtime: nodejs8.10
 stage: dev
 region: eu-west-1
functions:
 app:
   handler: index.handler
   events:
     - http: ANY /
     - http: 'ANY {proxy+}'
     - cors: true
Integrate Express
Inside index.js place the following:

// index.js
import serverless from "serverless-http";
import express from "express";
const app = express();
app.get("/", function(req, res) {
 res.send("Hello World!");
});
export const handler = serverless(app);
Configure Webpack
Create a webpack.config.js file

$ touch webpack.config.js
We will be using babel to transpile our code, so we will need a pull in the necessary loaders and packages

$ npm i -D babel-core babel-loader babel-plugin-source-map-support babel-preset-env
Add the following webpack config

// webpack.config.js
const path = require("path");
const slsw = require("serverless-webpack");
const nodeExternals = require("webpack-node-externals");
module.exports = {
 entry: slsw.lib.entries,
 target: "node",
 mode: slsw.lib.webpack.isLocal ? "development" : "production",
 optimization: {
   // We no not want to minimize our code.
   minimize: false
 },
 performance: {
   // Turn off size warnings for entry points
   hints: false
 },
 devtool: "nosources-source-map",
 externals: [nodeExternals()],
 module: {
   rules: [
     {
       test: /\.js$/,
       exclude: /node_modules/,
       use: [
         {
           loader: "babel-loader"
         }
       ]
     }
   ]
 },
 output: {
   libraryTarget: "commonjs2",
   path: path.join(__dirname, ".webpack"),
   filename: "[name].js",
   sourceMapFilename: "[file].map"
 }
};
Right, let’s give this a test and say hello to all the world

$ sls offline start
You should see the following output

Serverless: Bundling with Webpack...
Time: 645ms
Built at: 2018-04-20 00:41:52
      Asset       Size  Chunks             Chunk Names
   index.js   4.69 KiB   index  [emitted]  index
index.js.map  922 bytes   index  [emitted]  index
Entrypoint index = index.js index.js.map
[./index.js] 469 bytes {index} [built]
[express] external "express" 42 bytes {index} [built]
[serverless-http] external "serverless-http" 42 bytes {index} [built]
Serverless: Watching for changes...
Serverless: Starting Offline: dev/eu-west-1.
Serverless: Routes for app:
Serverless: ANY /
Serverless: ANY /{proxy*}
Serverless: (none)
Serverless: Offline listening on http://localhost:3000
Visit http://localhost:3000 in your browser and you should see Hello World!

Create React App
Next, let’s create our client — a create react app (CRA) in my-project root, use the create react app cli

$ create-react-app client
This should create a client directory containing a react application.

Express Middleware
We will use an Express middleware to do all the heavy lifting and render our react app.

Create a directory called middleware

$ mkdir middleware
Inside middleware, create a renderer.js file

$ touch renderer.js
Add the following code

// renderer.js
import fs from "fs";
import path from "path";
import React from "react";
import ReactDOMServer from "react-dom/server";
// import main App component
import App from "../client/src/App";
export default (req, res, next) => {
 // point build index.html
 const filePath = path.resolve("client", "./build", "index.html");
// read in html file
 fs.readFile(filePath, "utf8", (err, htmlData) => {
   if (err) {
     return res.send(err).end();
   }
   // render the app as a string
   const html = ReactDOMServer.renderToString();
// inject the rendered app into our html and send it
   return res.send(
     // replace default html with rendered html
     htmlData.replace('
', `
${html}
`)
   );
 });
};
Make sure you also install all the react dependecies

$ npm i -S react react-dom react-scripts
Great, now that we’ve created our renderer middleware, let’s update index.jsand use our renderer

// index.js
import serverless from "serverless-http";
import express from "express";
import path from "path";
// import middleware
import renderer from "./middleware/renderer";
const app = express();
// root (/) should always serve our server rendered page
app.use("^/$", renderer);
// serve static assets
app.use(express.static(path.join(__dirname, "client", "./build")));
// handler
export const handler = serverless(app);
We’re almost at the finish line. Let’s make a few changes to our webpackconfig first. Install the following loaders and plugins

$ npm i -D babel-plugin-css-modules-transform babel-preset-es2015 babel-preset-react-app babel-preset-stage-2 babel-preset-env url-loader copy-webpack-plugin
Update webpack.config.js to the following config

// webpack.config.js
const path = require("path");
const slsw = require("serverless-webpack");
const nodeExternals = require("webpack-node-externals");
const CopyWebpackPlugin = require("copy-webpack-plugin");
module.exports = {
 entry: slsw.lib.entries,
 target: "node",
 mode: slsw.lib.webpack.isLocal ? "development" : "production",
 optimization: {
   // We no not want to minimize our code.
   minimize: false
 },
 performance: {
   // Turn off size warnings for entry points
   hints: false
 },
 devtool: "nosources-source-map",
 externals: [nodeExternals()],
 module: {
   rules: [
     {
       test: /\.js$/,
       loader: "babel-loader",
       include: __dirname,
       exclude: /node_modules/,
       query: {
         presets: ["es2015", "react-app", "stage-2"],
         plugins: ["css-modules-transform"]
       }
     },
     {
       test: /\.(png|jp(e*)g|svg)$/,
       use: [
         {
           loader: "url-loader",
           options: {
             limit: 8000,
             name: "images/[hash]-[name].[ext]"
           }
         }
       ]
     }
   ]
 },
 plugins: [
   new CopyWebpackPlugin([{ from: "client/build", to: "build" }], {
     debug: "info"
   })
 ]
};
We employ the copy-webpack-plugin in order to copy our client build to the bundle we plan to deploy to AWS.

Test Locally
To deploy our serverless application locally, run

$ sls offline start
Visit http://localhost:3000 in your browser and you should see the default create react app :D.

Deploy
In order to deploy our amazing new serverless application to AWS, we first need to update the client package.json . Add the homepage property with the value “/dev”. This is so that our react app is aware of the base path enforced by API Gateways stages.

// client/package.json
{
 "name": "client",
 "version": "0.1.0",
 "private": true,
 "homepage": "/dev",
 "dependencies": {
   "react": "^16.3.2",
   "react-dom": "^16.3.2",
   "react-scripts": "1.1.4"
 },
 "scripts": {
   "start": "react-scripts start",
   "build": "react-scripts build",
   "test": "react-scripts test --env=jsdom",
   "eject": "react-scripts eject"
 }
}
Finally, let’s build and deploy

$ npm run build --prefix client && sls deploy -v
If everything goes according to plan, our create react app will build, webpack will bundle everything together and Serverless will orchestrate our serverless architecture as well as upload our bundle to a Lambda.

If Serverless successfully deploys your application, you should see your newly created Lambda function as well as API Gateway endpoint in the output.

Service Information
service: my-project
stage: dev
region: eu-west-1
stack: my-project-dev
api keys:
 None
endpoints:
 ANY - https://t4xx50pfme.execute-api.eu-west-1.amazonaws.com/dev
 ANY - https://t4xx50pfme.execute-api.eu-west-1.amazonaws.com/dev/{proxy+}
functions:
 app: my-project-dev-app
Navigate to your newly created endpoint and you should see your react app, which has been rendered and served from an Express app running inside a Lambda function.

Closing
We have successfully leveraged AWS Lambda in order to render and serve a create react app. This, obviously, might not be the best solution, but it definitely is an interesting one. Serverless architecture is an amazing technology and has great potential. So go forth and do more interesting things with serverless, today!

References

https://serverless.com/blog/serverless-express-rest-api/
https://medium.com/bucharestjs/upgrading-a-create-react-app-project-to-a-ssr-code-splitting-setup-9da57df2040a

As seen on FOX, Digital journal, NCN, Market Watch, Bezinga and more