WittCode💻

Create a React App with Routing from Scratch with Express

By

Learn to how to create a React App with routing from scratch using Express, Webpack, and React Router. We will also learn about client side vs server side routing in single page applications.

Table of Contents 📖

Environment Variable Setup

To begin, lets define the location of our Express server in environment variables. We'll also set the Node environment to development.

NODE_ENV=development

SERVER_HOST=localhost
SERVER_PORT=5678

These variables will be loaded into React using Webpack and the whole application using Node.

Project Setup and Webpack Installation

Next, lets initialize an empty directory as an ES6 npm project by running npm init es6 -y.

npm init es6 -y

Now lets install webpack and webpack-cli as development dependencies from npm. Webpack will allow us to turn our React code into JavaScript code that the browser understands.

npm i webpack webpack-cli -D

However, for it to do this, we need to install a Webpack loader, a function that Webpack uses to convert code from one form to another.

npm i babel-loader @babel/preset-env @babel/preset-react -D

Webpack will use the Babel loader to convert JSX code into code that the browser understands. Now we need to install a plugin for Webpack. A plugin allows us to plug into Webpack's lifecycle.

npm i html-webpack-plugin -D

The html-webpack-plugin will apply our bundled React code to an HTML file. Now lets install nodemon as a development dependency so we can reload our Express server whenever it changes.

npm i nodemon -D

Now lets set the entry point to our application and create a start script to run our Express server.

"main": "./src/server.js",
...
"scripts": {
  "start": "nodemon --env-file .env ."
},

INFO: Note that to use the --env-file flag we need to use Node version 20.11.0 or higher. This flag loads the environment variables in the specified file into our application.

Configuring Webpack

Now lets configure Webpack to use the loaders and plugins we just installed.

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  target: 'web',
  entry: './src/client/index.jsx',
  output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: '/',
    filename: 'bundle.js',
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/client/index.html',
    }),
    new webpack.EnvironmentPlugin([
      'NODE_ENV',
      'SERVER_HOST',
      'SERVER_PORT',
    ])
  ],
  module: {
    rules: [
      {
        test: /.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              '@babel/preset-env',
              ['@babel/preset-react', {'runtime': 'automatic'}]]
          }
        }
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx']
  }
};
  • mode - The mode to bundle the application in, such as development or production.
  • entry - The entry point to start the bundling process.
  • target - The environment we will be working in. We set it to web as React applications work in the browser.
  • filename - The name of the output code bundle.
  • path - The directory to output the bundle to.
  • publicPath - Tells Webpack where to output its bundled files to. Will be helpful when serving from Express.
  • plugins - Taps into the Webpack lifecycle. Here we're telling Webpack to place the bundled JavaScript in the provided HTML file. We also set some environment variables.
  • rules - Tell webpack how to handle a certain module. Here we are telling Webpack to pass any .js or .jsx files (excluding those inside node_modules) through the Babel loader.

Creating a React App with Routing

Now lets create our React application. First, lets install the libraries react and react-dom.

npm i react react-dom
  • react - The core library for React.
  • react-dom - Renders the React app in the DOM.

Now lets install the react-router-dom library from npm. This library contains all the bindings for using React Router in a web application.

npm i react-router-dom

React Router is a JavaScript framework that lets us handle client side routing in React. In other words, we can use it to create a single page app that provides navigation without a page refresh. Now lets fill in our index.html file to house the React application.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WittCode</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>

The most important part of this is the div element with the id root. That is what the react-dom package will use to create our React app. Next, create the glue between our DOM and React application.

import React from 'react';
import {createRoot} from 'react-dom/client';
import {createBrowserRouter, RouterProvider} from 'react-router-dom';
import Contact from './components/Contact';
import About from './components/About';
import Layout from './components/Layout';
import Home from './components/Home';

const router = createBrowserRouter([
  {
    element: <Layout />,
    children: [
      {path: '/', element: <Home />},
      {path: '/about', element: <About />},
      {path: '/contact', element: <Contact />},
    ]
  }
]);

const root = createRoot(document.getElementById('root'));
root.render(<RouterProvider router={router} />);
  • Import React libraries and components.
  • Create a Browser Router. This router allows us to add routing to our Web app.
  • Create a parent element called Layout that all child components will be rendered inside. This element will hold our navigation links.
  • Create a component for each route.
  • Pass the router to the RouterProvider component. This will tell React Router how to render the React app.
  • Render the React app into our div element using react-dom.
import {Outlet} from 'react-router-dom';
import Header from './Header';

export default function Layout() {
  return (
    <>
      <Header />
      <Outlet />
    </>
  );
}
  • Import the Outlet component. This tells the parent component where to render its children.
  • Import the Header component. This is the component that houses the navigation links.
import {Link} from 'react-router-dom';

export default function Header() {
  return (
    <>
      <nav>
        <ul>
          <li><Link to="/">Home</Link></li>
          <li><Link to="/about">About</Link></li>
          <li><Link to="/contact">Contact</Link></li>
        </ul>
      </nav>
    </>
  );
}
  • Import the Link component. This allows us to add client side routing without doing a full document request.
  • Create a link for each one of our routes.

Now lets create each of our components. Each component will also make an API call to our Express server using a custom hook.

import useFetch from '../hooks/useFetch';

export default function Home() {
  const data = useFetch('/api/home');
  return (
    <>
      <h1>Home</h1>
      <h2>API data: {data}</h2>
    </>
  );
}
import useFetch from '../hooks/useFetch';

export default function About() {
  const data = useFetch('/api/about');
  return (
    <>
      <h1>About</h1>
      <h2>API data: {data}</h2>
    </>
  );
}
import useFetch from '../hooks/useFetch';

export default function Contact() {
  const data = useFetch('/api/contact');
  return (
    <>
      <h1>Contact</h1>
      <h2>API data: {data}</h2>
    </>
  );
}

These API calls are made with a custom hook called useFetch. Lets create this hook real quick.

import {useEffect, useState} from 'react';

export default function useFetch(path) {
  const [data, setData] = useState({message: 'Loading...'});
  useEffect(() => {
    // Cancel the fetch request
    const controller = new AbortController();
    const {signal} = controller;

    fetch(`http://${process.env.SERVER_HOST}:${process.env.SERVER_PORT}${path}`, {signal})
      .then(resp => resp.json())
      .then(data => {setData(data);})
      .catch(err => console.error(err));

    return () => {
      controller.abort();
    };
  }, []);

  return data.message;
}
  • Create some state to hold our data.
  • Create an abort controller to stop the request if the component unmounts.
  • When the component mounts, we will make a fetch request to our Express server.

Creating an Express Server

Now lets create our Express server to serve up the React application we just made. To begin, lets install Express from npm.

npm i express

Now lets import Express and get the application listening for requests in our main server.js file.

import express from 'express';

const HOST = process.env.SERVER_HOST;
const PORT = process.env.SERVER_PORT;

const app = express();

// Logging middleware
app.use((req, res, next) => {
  console.log('-------------------------------------');
  console.log(`Request: ${req.method} ${req.url}`);
  return next();
});

// Add api routes
const ApiRouter = express.Router();
ApiRouter.get('/home', (req, res) => {
  return res.json({message: 'Home page data from Express!'});
});
ApiRouter.get('/about', (req, res) => {
  return res.json({message: 'About page API data oh yeah!'});
});
ApiRouter.get('/contact', (req, res) => {
  return res.json({message: 'This is Express! Here are the contacts!'});
});
app.use('/api', ApiRouter);

app.listen(PORT, HOST, () => {
  console.log(`Server started listening on ${HOST}:${PORT}`);
});

We also add a useful global logging middleware to see the requests coming in and create our API routes. Now, to serve our React app from this server, we need to get Webpack and Express to work together. We can do this with the npm package webpack-dev-middleware.

npm i webpack-dev-middleware -D

This package emits files processed by Webpack to a server. We will use it to emit our bundled React application to our Express server which we can then serve to the client. First lets import this middleware into our server.js file along with the webpack library itself and our Webpack configuration.

import webpack from 'webpack';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackConfig from '../webpack.config.cjs';

Now lets bind webpack-dev-middleware as application level middleware as we want it to be called each time a request is sent to the server.

const compiler = webpack(webpackConfig);
app.use(webpackDevMiddleware(compiler, {
  publicPath: webpackConfig.output.publicPath
}));
  • Create a Webpack compiler. When we import webpack directly we receive a function that returns a webpack compiler.
  • Configure the webpack compiler by passing it a Webpack configuration object. This is the object that we export from webpack.config.cjs.
  • Pass the Webpack compiler and options to the webpackDevMiddleware function.
  • Specify the publicPath to be the one in our Webpack configuration. This will be where webpack-dev-middleware serves the bundle from.

Adding Hot Module Replacement to Express

Now, to get our application to reload with changes, we need to use hot module replacement (HMR). HMR detects module changes in an actively running application. This makes the development process more efficient as it only updates what has changed and instantly updates the browser with the modifications. We can enable HMR in a custom Express server by using the npm package webpack-hot-middleware.

npm i webpack-hot-middleware -D

Now lets import the package into our server.js file.

import webpackHotMiddleware from 'webpack-hot-middleware';

Next, we need to add this middleware to the express server as application level middleware.

app.use(webpackHotMiddleware(compiler, {}));

The webpack-hot-middleware returns a function that accepts a webpack compiler as the first argument and options as a second argument. Next, inside our Webpack configuration file, we need to add the Webpack HMR plugin to the plugins array.

new webpack.HotModuleReplacementPlugin()

The HMR plugin enables Webpack to perform HMR. Finally, we need to alter our Webpack entry point slightly to contain the Webpack hot middleware.

entry: {
  main: ['webpack-hot-middleware/client?reload=true&timeout=2000', './src/client/index.jsx']
},

This code configures our Express server to receive a notification when Webpack builds the client bundle. In other words, when our React application is rebuilt, our Express server will be notified of this, causing it to serve up this new bundle. The reload=true is an option that auto-reloads the browser page when Webpack gets stuck. The timeout=2000 tells Webpack to wait 2 seconds after a disconnection before attempting to reconnect.

INFO: Setting this timeout will stop the server from hanging indefinitely which can occur when using routing.

Now all we need to is run the application with npm start.

npm start
...
[nodemon] starting `node --env-file .env .`
Server started listening on localhost:5678

Image

We can see our navigation and API calls are all working! However, if we try to navigate to a different page using the URL bar, we get a 404 error from Express. This is because we are using a single page application. Express is looking for something served up on the /about and /contact routes but we are only serving our SPA, index.html file, on the route /.

INFO: To serve up our React app on those routes, we need to redirect these requets to where our index.html file is: the / route.

Redirecting Calls to index.html

To further demonstrate this, lets change our Header.jsx file to use anchor tags as opossed to React Router's links. This will make the browser request a new HTML page from our Express server.

export default function Header() {
  return (
    <>
      <nav>
        <ul>
          <li><a href="/">Home</a></li>
          <li><a href="/about">About</a></li>
          <li><a href="/contact">Contact</a></li>
        </ul>
      </nav>
    </>
  );
}

To reroute our requests to the index.html file, we need to use the connect-history-api-fallback middleware.

npm i connect-history-api-fallback -D

This middleware changes the requested location of the single page application whenever there is a request that follows the following rules:

  • The request is a GET or HEAD request that accepts text/html.
  • The request is not a direct file request, i.e. the requested path does not contain a . (DOT) character.
  • The request does not match a pattern provided as a rewrite.

Of course, these rules are configurable. But first, lets import this library at the top of our server.js file.

import historyApiFallback from 'connect-history-api-fallback';

Now place this middleware before the webpack-dev-middleware.

app.use(historyApiFallback({
  verbose: true,
  rewrites: [
    {
      from: /^/api/.*$/,
      to: (context) => context.parsedUrl.path
    }
  ]
}));
  • Add some useful logging to the console.
  • We don't want to rewrite our API calls so we set a rewrite rule for them.

Now just run the application again with npm start.

npm start
...
[nodemon] starting `node --env-file .env .`
Server started listening on localhost:5678
-------------------------------------
Request: GET /api/about
Rewriting GET /api/about to /api/about
-------------------------------------
Request: GET /contact
Rewriting GET /contact to /index.html
-------------------------------------
Request: GET /bundle.js
Not rewriting GET /bundle.js because the path includes a dot (.) character.
-------------------------------------
Request: GET /__webpack_hmr
Not rewriting GET /__webpack_hmr because the client does not accept HTML.
-------------------------------------

Image

Notice how when we click an anchor tag a new HTML page is requested from Express. Thanks to our history fallback middleware, we are able to serve up the index.html file from these routes while maintaining our API route locations.

Create a React App with Routing from Scratch with Express