React SPA with server-side rendering on AWS Lambda

February 5th, 2020 • 1152 Words

React is great for creating websites and writing applications using JavaScript. But, whenever the use of React is rejected, one of the most common reasons is the lack of a simple implementation for server-side rendering (SSR). When you search on Google for this topic, you’ll find various approaches and most of the time, people will tell you it’s complicated. But Why?

This guide is also available in German on superluminar.io!

On GitHub, I published cra-serverless to showcase an approach to build your own pre-rendering for an unejected React application built with create-react-app. With AWS Lambda, Amazon CloudFront, and Amazon S3, you can host a static React SPA, have serverless pre-rendering, and enjoy the safety of having pay-per-use services.

cra-serverless for serverless pre-rendering using React

It may sound strange to use AWS Lambda for this in the beginning, but in the end, all you have to do is handling events that have a path parameter and respond with the generate HTML code. That’s what web servers usually do! Plus, they take care of all the things that you do not want to think about anyway. Like SSL handling, load balancing and auto-scaling, or content encoding and setting cache headers.

If you want to skip all the explanation and ideas behind this concept, just head over to GitHub and have a look at the repository. The result of cra-serverless looks like this: d31tuk9nqnnpkk.cloudfront.net

Frameworks And Dependencies

When you start a new project using React, chances are high you will end up using create-react-app. For styling, you might go for styled-components and your API communication could rely on GraphQL using AWS AppSync. All of them work fine if you implement them for running in a web browser. But did you know, and please fasten your seatbelts, all of them work totally fine with server-side rendering!?

All needed features are already in the core React packages. No additional framework is needed to render React components to static HTML code. Even styled-components already has all the needed functions to extract CSS rules. A few months ago, I already published an example project on GitHub about server-side rendering with React. With cra-serverless, those approaches are bundled into a full-featured example architecture using AWS.

Basic Theory And Routing

Most React applications rely on the react-router-dom package to handle navigation in the web browser. Yours does this too, right? Nice! Somewhere in your application, there is a BrowserRouter component:

import React from 'react'
import { BrowserRouter } from 'react-router-dom'

React.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
)

To build a serverless pre-rendering service, use the same implementation but use the StaticRouter instead and provide a location property. If you keep up with the common best-practices for React, your code will most-likely already be compatible without any additional changes needed.

import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom'

const markup = renderToString(
  <StaticRouter location={path}>
    <App />
  </StaticRouter>
)

When using Amazon API Gateway, you have scalable HTTP services at hand and you do not need to think about serving HTTP requests at all. Just pass the payload to your HTML renderer or respond with already cached data. The same is true for Amazon S3 when hosting static files and assets.

The missing piece of the puzzle is a service for routing static assets and dynamically pre-rendered HTML code. Amazon CloudFront can do this!

Things Get Real

If you use create-react-app you end up with a simple index.html file after running the yarn build command. To enable server-side rendering, you only need to use the generated index.html file and replace the empty body tag with the generated static HTML code of your components after using renderToString.

Next, use some framework to handle events with a path paremeter, like express or the lightweight koa. All compoments together, a solid architecture for serverless pre-rendering with React on AWS Lambda can look like this.

import { readFileSync } from 'fs'
import React from 'react'
import { renderToString } from 'react-dom/server'
import { HelmetData } from 'react-helmet'
import { HelmetProvider } from 'react-helmet-async'
import { StaticRouter } from 'react-router-dom'
import { ServerStyleSheet } from 'styled-components'

const html = readFileSync('../build/index.html').toString()

export const render = (Tree: React.ElementType, path: string) => {
  const context = { helmet: {} as HelmetData }
  const sheets = new ServerStyleSheet()

  const markup = renderToString(
    sheets.collectStyles(
      <HelmetProvider context={context}>
        <StaticRouter location={path}>
          <Tree />
        </StaticRouter>
      </HelmetProvider>
    )
  )

  return html
    .replace('<div id="root"></div>', `<div id="root">${markup}</div>`)
    .replace('<title>React App</title>', context.helmet.title.toString())
    .replace('</head>', `${context.helmet.meta.toString()}</head>`)
    .replace('</head>', `${context.helmet.link.toString()}</head>`)
    .replace('</head>', `${sheets.getStyleTags()}</head>`)
    .replace('<body>', `<body ${context.helmet.bodyAttributes.toString()}>`)
}

As you can see, no additional libraries or frameworks are needed. All packages that you already use provide the features to have pre-rendered HTML responses for your application. Next, wrap this code in some kind of HTTP router:

import koa from 'koa'
import http from 'koa-route'
import serve from 'koa-static'

import App from '../src/App'

export const Router = new koa()

Router.use(
  http.get('*', (ctx: koa.Context) => {
    ctx.body = render(App, ctx.request.path)
  })
)

With a dedicated router for HTTP requests, you can spin up a local development server or use the serverless-http package to configure your service to support incoming HTTP requests from Amazon API Gateway.

// AWS Lambda
import serverless from 'serverless-http'
import { Router } from './router'

export const run = serverless(Router)
// Local HTTP server
import { Router } from './router'

console.log(`🎉 Starting HTTP server at http://localhost:3000`)
Router.listen(3000)

That’s all you need for adding serverless pre-rendering to your existing React application built with create-react-app. As mentioned in the beginning, even frameworks like styled-components or Apollo Client for using GraphQL with AWS AppSync work fine with this approach.

Infrastructure and Deployments

For managing all components of the needed infrastructure, cra-serverless uses the AWS Cloud Development Kit. Using the CDK, you can create resources in AWS using a common programming language, like TypeScript. Together with AWS CodePipeline and AWS CodeBuild you can configure and maintain the whole process of deploying your application in the same GitHub repository. Thanks to GitHub Webhooks you can easily trigger AWS CodePipeline and enjoy a continuous deployment.

A pipeline using AWS CodePipeline consists of multiple Stages, each may contain multiple Actions that either are processed in parallel or sequential order. This first Stage of a pipeline usually is used to download all source files. When the Action for downloading the source files has finished, all files are stored in an S3 Bucket for further processing in one of the following steps.

AWS CodePipeline triggered by GitHub Webhook

After the sources have been downloaded, the pipeline can start to prepare all the necessary components of the architecture. The configured Actions for the next Stage use

The Actions to build the React SPA and for compiling the sources for AWS Lambda function are handled in sequential order as the static files from create-react-app are needed in the AWS Lambda function. The generation of the needed CloudFormation templates is processed in parallel, as there are no dependencies needed for this task. After each Action has finished, the generated files are stored in S3 again.

AWS CodePipeline Actions using AWS CodeBuild to build create-react-app with server-side rendering

Now, with all sources and generated files available in S3, it’s time to deploy everything to its final destination! When deploying a React application with serverless pre-rendering and static assets, it’s important to take care of the correct order of the deploy tasks to avoid any potential downtime.

The first Action in the Stage of deployment is to copy all static files created with create-react-app to the final S3 Bucket used with CloudFront. After all new static files are copied, the sources of the AWS Lambda function will be deployed using the generate CloudFormation templates. The last step to deploy the application is configuring the CloudFront CDN.

AWS CodePipeline to deploy a create-react-app static website with server-side rendering

As soon as all three Actions have finished, it’s time to invalidate all outdated assets that may be stored in the cache of CloudFront. Done, your React SPA is now served using CloudFront and has serverless pre-rending! This guide is also available in German and you can access the deployed React application at d31tuk9nqnnpkk.cloudfront.net


View on GitHubSource code is published using the MIT License.
  • Deploy React SPA with CodePipeline and CodeBuild using AWS CDK

    January 23rd, 2020 • 748 Words • EN

    There are plenty of tools and services for continuous delivery available. Most of them are either directly built into the source code management tools you already use, or perfectly integrate with them. You might be familiar with CircleCI, Travis CI, GitLab CI, or GitHub Actions. Of course, AWS has…

  • AWS re:Invent 2019 Recap & Videos

    December 15th, 2019 • 360 Words • EN

    I was able to attend the AWS re:Invent 2019 conference. A week full of learning about current and new technologies, services, and general approaches is definitely overwhelming. There is no much content available, during the conference, and as videos and slide decks afterwards. I tried to list my…

  • CloudFormation Best-Practices

    May 1st, 2019 • 457 Words • EN

    You can find plenty of frameworks and tools to provision your AWS resources. Some of them do a great job for a specific purpose, others are more generic. Nevertheless, I do prefer to use native CloudFormation templates as much as possible. Prefix all the things Split up your CFN Stacks Nested…

  • Makefile Best-Practices

    April 30th, 2019 • 167 Words • EN

    The more projects you work on, the more streamlined your tooling gets. Hopefully. Various services using different languages have different tooling requirements, of course. A sweet Makefile can be the entry to a unified tooling interface. Hide Commands Wildcard Targets Foreach and Lists As long as I…

  • AppSync GraphQL API with Custom Domain and CloudFormation

    April 7th, 2019 • 165 Words • EN

    With AWS AppSync, it’s easy to run your own serverless GraphQL service API. Thanks to Velocity Mapping Templates, DynamoDB, and AWS Lambda your can aim for an architecture without any maintenance at all. Getting started with AppSync is not that problem; there are tons of guides and frameworks. AWS…

  • Last Update: 2020-02-17T10:07:11.183Z
    Stresemannstraße 132, 22769 Hamburg
    +49 151 54 64 90 55
    inbox@sbstjn.com