πŸ— Creating An Express Server

Photo by Kelly Sikkema on Unsplash

This is part 4 of my series on server-side rendering (SSR):

  1. πŸ€·πŸ»β€β™‚οΈ What is server-side rendering (SSR)?
  2. ✨ Creating A React App
  3. 🎨 Architecting a privacy-aware render server
  4. [You are here] πŸ— Creating An Express Server
  5. πŸ–₯ Our first server-side render
  6. πŸ– Combining React Client and Render Server for SSR
  7. ⚑️ Static Router, Static Assets, Serving A Server-side Rendered Site
  8. πŸ’§ Hydration and Server-side Rendering
  9. 🦟 Debugging and fixing hydration issues
  10. πŸ›‘ React Hydration Error Indicator
  11. πŸ§‘πŸΎβ€πŸŽ¨ Render Gateway: A Multi-use Render Server

Over the previous three posts in this series we have described what server-side rendering (SSR) is, created a simple application using React, and discussed the architecture of a privacy-aware server to ensure we understand some of the sharp edges around SSR. Now, we will actually implement a basic server. Just as with the React application we created, the server we create will not be a complete solution, but it will provide a foundation from which we can continue to explore SSR.

✨ A New Project

Where do we start? Well, we need a server that can receive web requests and respond to them. For that, I am going to use Express1, but first I need a project.

NOTE: Where you see yarn, know that you can use your own package manager as you see fit.

  1. Add a new repository on GitHub (or your source control platform of choice).
  2. Make a new folder locally for your code
  3. cd to your new folder and run git init
  4. git remote add origin <your github repo URL>
  5. git pull origin master
  6. git branch --set-upstream-to=origin/master
  7. Create and commit a .gitignore file
  8. Initialize it for JavaScript package management with yarn init
  9. Run yarn install to generate the lock file
  10. Commit the yarn.lock and package.json to the git repository

Great, so now we have a project we can start working on. Let's add Express.

yarn add express

This should update our package.json and yarn.lock, so don't forget to commit those changes. I also recommend pushing often to your remote repository, that way your code is backed up online in case your computer suffers a nasty accident2.

πŸ‘‹πŸ» Hello World!

At this point we need to write some code. We need to setup a route for our server that can handle providing a rendered result for any URL that our application might have. There are a couple of ways we could do this:

  1. Assuming that our server is invoked by some intermediate layer, such as a cache, we could have the server implement a single route (e.g. /render) and pass the URL to be rendered as a query parameter.
  2. Our server could assume the URL is to be rendered by the client code and just accept any URL.

Option 1 gives us a great deal of flexibility in what our server can do, but it forces us to ensure that there is a layer between the original browser request and our server, as something has to be responsible for constructing the appropriate /render route request. Option 2 removes the need to have an intermediate layer, but it perhaps restricts us from expanding server functionality. Of course, option 2 can be changed to option 1 if the need arises, so we can go with option 2 for now, knowing that later, it can be updated to suit changing needs.

Normally, I would add lots of other things to this server to improve development and runtime investigations, such as linters, testing, and logging, but for the sake of brevity, right now we will stick to the main functionality.

const express = require("express");

const port = 3000;
const app = express();

app.get("/*", (req, res) => res.send("Hello World!"));

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

This is our index.js file. It is not doing a lot. On line 4, we create our express app. On line 6, we tell it that for any route matching /*, return Hello World!. On line 8, we tell it to listen for requests on port 30003.

If we run this app with node index.js, we can go to our browser, visit any route starting with localhost:3000 and see the text, Hello World!. This is fantastic. We have a server and it is responding as we hope. Since we are going to run this often as we make changes, I will add a script to our package.json to run node index.js for us.

{
  "name": "hello-react-world-ssr",
  "version": "0.0.1",
  "description": "A server-side rendering server",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "express": "^4.17.1"
  },
  "scripts": {
    "start": "node index.js"
  }
}

In the package.json file shown above, I have highlighted the section I added containing the new start command. From now on, we can start our app with yarn start. The next step is getting our server to render our React application. Before we do that, consider these questions:

  1. How does the server know about and load the code for our React application?
  2. How does the server get the rendered result to send back?
  3. How do we isolate render requests to avoid side-effects bleeding across requests?

πŸ€” The Hows

The answers to the first two questions have implications beyond the server itself, possibly influencing both our client application and any deployment process.

How our server knows about and loads our client application may affect how our server is deployed. Some server-side rendering solutions involve deploying the client-side code with the server so that it has direct access to the appropriate code, others use a mechanism such as looking up in a manifest to identify the files to load from a separate location (such as a content delivery network (CDN)). Neither of these is necessarily a bad choice – they both have their advantages and disadvantages. For example, deploying the server with the right code means:

  • βœ…The server has fast access to the client application it is rendering
  • βœ…The server can integrate nicely with the client application
  • ❌The render server must be deployed every time the client application changes
  • ❌The server is closely coupled to the client application

Whereas, looking up files in a manifest and loading them from elsewhere means:

  • βœ…The render server rarely requires updating
  • βœ…The server can render more than one application
  • ❌The server will probably need to cache JavaScript files locally or be at the mercy of latency when communicating with the CDN
  • ❌The client applications that the server renders likely need to include custom code to support being rendered by that server

Being aware of these approaches differ – and they differ in more than just the ways I have suggested, is useful in understanding the trade-offs we must make when implementing our render server. Perhaps answering the second question will help us decided which route to take; consider how will our server get a rendered result of the client application?

Our server is going to invoke a call from the React framework that renders our React application to a string, rather than mounting it inside the DOM of a browser. To do that, it needs a React component to render, so it must load our client application and get the root-level component. In addition, assuming our render server is rendering the entire page and not just the React component, the server is likely going to need to gather additional information, such as which files must be loaded in the page, the page title, etc.

This whole process of capturing the application render and associated metadata requires interplay between the server code and the client code. Revisiting the first question and the two approaches I gave: if the server has the client code deployed with it, the server could know exactly which files to load to render the component, importing those directly and using them accordingly; if the server is less-closely coupled, we likely need some mechanism whereby the client application itself does more of the heavy lifting by hooking into some framework provided by the server, even if that is just exporting a specific object so that server can identify the appropriate things to coordinate rendering.

Ultimately, either we have a server that is custom built to our application, or we have a server that is built to support many applications. What to do? I say, dive in and try them both. To that end, next time we will look at the first option where the server knows all about the client application (though we may cut some corners to get to the salient points), and we will answer that third question; how do we isolate our renders?

πŸ™‡πŸ»β€β™‚οΈ In Conclusion

Herein we have created our server, though it does not do much yet. We have also considered two different approaches to connect our server to our client application: closely-coupled or more open, and we have started to think about how the server will isolate and respond to render requests.

This week's entry turned out a little longer than I had intended, and covered less things than I had hoped. Sometimes that is the way it goes. One of the biggest reasons I write these blogs is to discover what I do and do not know about something. Often in the effort of explaining it to someone else, I identify a bias that I have without any supporting evidence, or a topic I grasp that is far harder to explain than I expected.

Until next time, when we start to implement our server-side rendering, please leave a comment. Perhaps you have a question, a personal experience writing a render server, or want to take umbrage at something I have stated. I look forward to learning with you as we continue this journey into the land of SSR. πŸ—Ί

  1. I find Express easy enough to use and well-supported, though there are other options that one could use instead if one were so inclined []
  2. A lesson from bitter experience; hard drives die (especially SSDs) without warning, drinks spill, laptops get dropped – keep your work backed up []
  3. The port is currently hard-coded for simplicity, but we could make this configurable []

🎨 Architecting a privacy-aware render server

Photo by Sergey Zolkin on Unsplash

This is part 3 of my series on server-side rendering (SSR):

  1. πŸ€·πŸ»β€β™‚οΈ What is server-side rendering (SSR)?
  2. ✨ Creating A React App
  3. [You are here] 🎨 Architecting a privacy-aware render server
  4. πŸ— Creating An Express Server
  5. πŸ–₯ Our first server-side render
  6. πŸ– Combining React Client and Render Server for SSR
  7. ⚑️ Static Router, Static Assets, Serving A Server-side Rendered Site
  8. πŸ’§ Hydration and Server-side Rendering
  9. 🦟 Debugging and fixing hydration issues
  10. πŸ›‘ React Hydration Error Indicator
  11. πŸ§‘πŸΎβ€πŸŽ¨ Render Gateway: A Multi-use Render Server

This week, we are back to our adventures in server-side rendering (SSR). So far in this series, we have looked at:

At this point in our journey into SSR, we need a server. Before we make a server, we must consider privacy and performance so this post is going to be light on code as we talk through important considerations. Though these considerations can be dealt with later, thinking about them upfront will help us avoid some really big pitfalls.

πŸ“ˆ Performance

All our initial considerations regarding server architecture come down to performance. Performance is the reason we want to implement SSR. We want to reduce the time our users must wait to see and interact with our site. Since the render of a page, whether in a browser or in our render server, can take variable amounts of time based on network latency, data requirements, and more, this inevitably means caching. Caching also helps us reduce costs since the render server will do less work. Whether caching is provided by your own implementation or via a service like Fastly, it has implications on what we should render on the server.

  • To get the most from our SSR solution, we need to render as much of a page as we possibly can.
  • To get the most from our caching, we want to share renders among as large a user base as we can so that a single render provides a cache hit for many people.

These two points compete against one another. In an ideal SSR world, for any given route the entire page is rendered so that the client does as little work as possible, but in an ideal caching world, a single cached entry for a route is shared with every user so that it is rendered once and then just shared. Assuming our app changes the page based on information about the user, both general – what device they are on, whether they are logged in or not, etc., and specific – what their name is, where they live, etc., we only get the most from caching if we do not render differentiating information during SSR such that we can share that render with more users. I doubt there is a hard and fast rule about how to find that caching sweet spot between being too specific about cache keys or being too general about rendering, but we can make things easier for ourselves.

First, we must ensure the cache key only includes generalizations and non-user-specific info.

Second, we introduce means to make generalizations about the user that can be a part of the cache key (is logged in, using Android, etc.) and that we can then pass to the render server to influence what is rendered.

Combining these two pieces of generalizing users and using that to influence the cache keys enables us to maximize what we can render while providing cacheable results that can be shared without leaking personal information. However, since the generalized information must be calculated and it can influence the cache key, there are implications for our architecture; if the render server does the generalization work, how can it tell the cache about it? We will look into that later, but for now, let's just note it as a problem to be solved.

πŸ“Š Data Access

At this point, we have worked through how to architect our server to support caching via generalizing users so that cache hits are likely without leaking personal information. That's great, and in fact, one might think, unnecessary. As long as user-specific information is not available to the render server, we cannot possibly render user-specific data. In many apps, data is obtained via REST or GraphQL and as such, is asynchronous. Since our render server is only rendering the very first render of a page, any asynchronous data will not be resolved and therefore not included in the result. So, where's the problem?

Well, not all data is asynchronous; some may be in the request itself, and even if we don't cache based on it, we must still ensure it is not rendered in the result. To mitigate this, we can use frontend components that wrap access to user-specific information so that we have a consistent rendering experience. Whether rendering for the first time in the client or the first time on the render server, these components will ensure the right thing is rendered and that the client will render the real data. Such data access components might render a spinner or some other placeholder that represents the currently unusable personal information (such as username) until the data is available. This means that all the considerations for rendering the right thing, whether in a browser or in the render server, are codified within the React app itself.

By using components in conjunction with linters and tests, we can enforce a data access policy that enables us to SSR effectively while respecting and enforcing user privacy.

πŸ“ƒ In Summary

That's a lot of words. Hopefully it has clarified some of the more jagged edges to SSR that often do not seem apparent until much later. It is much easier to make considerations about these things up front1.

In discussing how our render server will work with respect to a cache and user privacy, we have uncovered some details that affect not only the render server, but also our React app:

  1. The render server should sit behind a cache for performance, cost, and other benefits
  2. User-specific data like names, location, etc. should be omitted from the SSR result, cache keys, and initial client-side render
  3. It is useful to allow generalized user information in SSR, but it must also form part of the cache key
  4. Using React components, linters, and tests can enforce your policy around rendering user-specific content

That is it for this week. I thought I would get around to implementing the first version of our server, but I did not, so we will get into that next time. In the meantime, here are some things to consider that we will likely address in upcoming posts.

  1. Given that SSR only performs the very first render of our React app, how we can include asynchronous data and render more of our page?
  2. How will our render server know what browser code to render?
  3. How can generalizations about users be included in the cache key and used by the render server?
  1. something I have painfully learned by not doing so []

✨ Creating A React App

Photo by Rolands Zilvinskis on Unsplash

This is part 2 of my series on server-side rendering (SSR):

  1. πŸ€·πŸ»β€β™‚οΈ What is server-side rendering (SSR)?
  2. [You are here] ✨ Creating A React App
  3. 🎨 Architecting a privacy-aware render server
  4. πŸ— Creating An Express Server
  5. πŸ–₯ Our first server-side render
  6. πŸ– Combining React Client and Render Server for SSR
  7. ⚑️ Static Router, Static Assets, Serving A Server-side Rendered Site
  8. πŸ’§ Hydration and Server-side Rendering
  9. 🦟 Debugging and fixing hydration issues
  10. πŸ›‘ React Hydration Error Indicator
  11. πŸ§‘πŸΎβ€πŸŽ¨ Render Gateway: A Multi-use Render Server

Last week, I gave my own brief history of web sites and how their frontend implementation has drifted from entirely server-based, to entirely client-based, and is now settling (perhaps) with hybrid that we call server-side rendering (SSR). The goal of this journey is to poke around the gnarly bones of SSR and learn what we learn. We may make mistakes, break idioms, and portray ourselves as fools, but we will definitely learn. For that reason, we are not going to bother with things like Next.js, which have already fleshed over and hidden away the gnarly bones for us1.

Now, before we dip even further in the specific world of SSR, we are going to need an app. To be specific, we are going to need a React app.

Hello, React World!

Before creating a React app, we need a package manager; either npm or yarn will suffice. Though we could put together our own app from scratch, there is no need to as the handy create-react-app package exists. There are numerous ways to use this, but the easiest is to use yarn create or npx, which will do the work of obtaining the package and executing it all in one go.

For yarn, drop create from the front of create-react-app:

yarn create react-app <app-name-here>

For npx:

npx create-react-app <app-name-here>

I want to also put this in a git repository so I can track my changes. I would normally make a directory, run git init and then get started. In this case, we do not need to as create-react-app takes care of that for us.

So, let's begin. Open a terminal and invoke create-react-app.

yarn create react-app hello-react-world

After executing this, you will have a working React app that uses react-scripts to manage the basics. This is perfect for our initial journey. If we navigate to that directory and run the project, we can see our app in action.

cd hello-react-world
yarn start
Screenshot of the website created by the create-react-app tool. It shows the React logo in blue, over a dark grey background, with the text "Edit src/App.js and save to reload." and a link labelled, "Learn React"

Fantastic. We have an app. Before we do any more, let's get some remote source control underway. I am paranoid of my machine dying and losing all my work, so having an off machine place to store things is really useful. First, add a new repository on your source control site of choice (I prefer GitHub). Second, connect the local repository to the remote one:

git remote add origin <repo-url>
git fetch
git branch --set-upstream-to origin/master master

Since we want our local code to be the first commit and we're pushing to a brand new repository, we can force push what we have.

git push -f

Routing

Okay, we have an app and it is in source control. This is usually a good spot to spend some time setting up code quality tools like eslint and prettier. I am going to be naughty and skip right over that right now and save it for a different post, perhaps. Instead, let us add some routing to our fledgling application.

There are a few options for implementing routing in a React application (some frameworks, like Next.js, even provide it out of the box). We are going to use React Router. There are two variants of React Router; one for React on websites, and one for React Native on mobile. We want the website variant, which is provided by the react-router-dom package.

yarn add react-router-dom

Now we will edit our app to have a couple of routes. The main app is defined in the src/App.js file. It should look something like this. I have highlighted the lines we're going to replace; we are also going to add some too.

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

There are two things we want to add.

  1. The routes to render our pages.
  2. The links to navigate to our routes.

First we import four things from react-router-dom:

  1. BrowserRouter
    This is the root of our React Router-based navigation. Basically, the router is responsible for the routing (I'm sure you guessed that).
  2. Link
    This replaces the anchor tag (<a>) for our navigation.
  3. Route
    This is used to render a matched route.
  4. Switch
    This allows us to specify a table of possible routes that can be used to work out what should handle the URL currently being viewed.

With these things, we can then add some routes. I am adding Home, About, and Contact. Here is my app code after the edit. I have highlighted the new lines.

import React from 'react';
import {BrowserRouter, Link, Route, Switch} from "react-router-dom";
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <BrowserRouter>
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <div className="App-links">
            <Link className="App-link" to="/">Home</Link>
            <Link className="App-link" to="/about">About</Link>
            <Link className="App-link" to="/contact">Contact</Link>
          </div>
        </header>
        <section className="App-content">
          <Switch>
            <Route path="/about">
              This is the about page!
            </Route>
            <Route path="/contact">
              This is the contact page!
            </Route>
            <Route path="/">
              This is the home page!
            </Route>
          </Switch>
        </section>
      </div>
    </BrowserRouter>
  );
}

export default App;
Screenshot of the updated app using React router. The top shows the React logo with three links below it for "Home", "About" and "Contact" all on a dark grey background. Below that is a white region with the text "This is the home page!"

I also edited the CSS a little, but only to make things easier to see. The important bits are the router, wrapping our app, the Link components to perform navigation, and the Route components that render each route. With this, we now have a single page React app that has three pages for home, about, and contact.

This is going to be the application we will eventually render on the server. The important take away at this point is that we are not going to change the functionality of this app in order to achieve our aim. There are some changes we must make to support SSR, but we will not have two versions of the code. The code that runs in the browser will run on the server.

Next time, we are going to setup a server that will perform our SSR and consider what changes we need to make to our application infrastructure in order to support it. We might even get our first server-side rendered page. Until then, thanks for joining me on this continued exploration of server-side rendering using React.

  1. of course, if starting a new project knowing you need SSR, you should explore solutions like Next.js []

πŸ€·πŸ»β€β™‚οΈ What is server-side rendering (SSR)?

Featured image modified from photo by Andre Mouton on Unsplash

This is part 1 of my series on server-side rendering (SSR):

  1. [You are here] πŸ€·πŸ»β€β™‚οΈ What is server-side rendering (SSR)?
  2. ✨ Creating A React App
  3. 🎨 Architecting a privacy-aware render server
  4. πŸ— Creating An Express Server
  5. πŸ–₯ Our first server-side render
  6. πŸ– Combining React Client and Render Server for SSR
  7. ⚑️ Static Router, Static Assets, Serving A Server-side Rendered Site
  8. πŸ’§ Hydration and Server-side Rendering
  9. 🦟 Debugging and fixing hydration issues
  10. πŸ›‘ React Hydration Error Indicator
  11. πŸ§‘πŸΎβ€πŸŽ¨ Render Gateway: A Multi-use Render Server

One of my main responsibilities at work involves server-side rendering (SSR). From managing the services that perform SSR to the client components that developers use to build SSR-able frontends, I have my focus on many pieces of our frontend infrastructure. In this series of posts, I want to share some of the things I have learned and perhaps demystify this mostly fantastic approach to creating performant, stable, web experiences.

When the Internet started coming alive the first time, a lot of the magic was implemented on servers (aka server-side or "on the backend") that built HTML pages to deliver to web browsers. During this period, impressive collections of user interface components were created to make developing these server-based web apps easier and more reliable. Sadly, sites often felt a clunky and slow because the browser just was not equipped to do much beyond rendering the HTML it was given; JavaScript execution was too slow for anything very meaningful. Even button clicks in the browser would cause a new request to the server that would then generate a whole new page for the browser.

Then Google Chrome and the v8 JavaScript engine came along and changed everything. Browsers became blessed with speedy JavaScript engines. That meant we could do a lot of this work in the browser (aka client-side or "on the frontend") and develop applications that could properly divide presentation (the application running in the browser) from data (the database and CRUD1 operations running in the backend). From this new power came the concept of the single page app (SPA), where one page comes from the server and then does most of its work client-side, deferring to the backend only when data is read or written. Often, the page is received from the server in an initial state and then subsequent data requests may populate that page (imagine your Facebook feed loading) to get it ready for you to use. However, this can mean that the time to interactive – the length of time before a user can actually use the page – is long. This affects all sorts of things, but particularly user retention. Folks don't like waiting and if they wait too long, they become frustrated and eventually bounce2.

Much like in the backend era prior to faster frontend JavaScript execution, new frameworks and user interface components have appeared that help to create powerful web apps using browser-based JavaScript. Things like Angular, Ember, and React (there are more – there are always more3). However, there can still be a mismatch between backend and frontend. To get a nice experience for our users, code runs on the backend to build an initial page and then that is handed to the frontend, which promptly takes over. Sometimes, this transition is nice and smooth, but other times it is not. More importantly, there are at least two different code paths for generating the page; at least one backend one and at least one frontend one4.

Having more than one code path trying to do equivalent work is hard to maintain. A change in one place may or may not need a change in the other, and either way, careful quality engineering is needed to make sure bugs are not introduced. The shift away from web apps executing entirely on the backend but rendered on the frontend to being executed mostly on the frontend with a bit of backend increased the complexity of the code for anyone that wanted a performant, engaging web site. You had a choice; either keep the separation of frontend for presentation and backend for data, and have a slower initial website experience, or blur the line and have more complex code, but a nicer user experience. Thankfully, folks thought about this and like those responsible for React, came up with a solution – server-side rendering (SSR).

Thanks to the JavaScript Revolution that started with v8, we now are able to run JavaScript outside of our web browsers (using NodeJS, for example). This creates some interesting opportunities for running the same code in both the frontend and backend. This does not mean that all the code would run in two places – we may want to keep the CRUD operations as a backend thing; however, being able to run our presentation code in both places means we can overcome some of that delay when a user first visits a page of our site. We can use the same JavaScript that would render our page in the web browser to render a version of our page on the server and then let the browser take over, all with a single codebase5.

Server-side rendering (SSR) – The rendering of a web page on a server rather than in a browser

And in the context of what I want to write about, that is server-side rendering (more commonly referred to as SSR, at least by me, anyway) – the rendering of a web page on a server rather than in a browser. In fact, it's so similar to rendering in a web browser, I have started to refer to the server responsible for SSR as a server-side browser. This tends to reframe how folks think of problems they face and how to start thinking about frontend code not as "does this run in the server or the client?" but "what browsers does this have to support?". It turns out that second question is much more familiar to most frontend developers than the first.

For now, I will leave things there. I think this post is quite long enough. Thank you for reading. Over the next few posts, we will look at creating an app using React that supports SSR, as well as a backend browser to perform that SSR, and the implications that SSR has when it comes to writing frontend code.

  1. Create Read Update Delete []
  2. there's a reason it's called "bounce rate" []
  3. While I was writing this, I expect three more frontend frameworks came into being and at least one died []
  4. There are cases where different parts of the same page are rendered by different services; front or backend – talk about complicated []
  5. Not only that, but the server response could be cached with a CDN (content delivery network) to make our sites even faster! []