Deploying a Multi-Route React App: Combining Express and Static Build
Introduction
Deploying a React application can be a daunting task, especially when it comes to serving multiple routes from a Node/Express server. Fortunately, with the help of a few libraries and some careful configuration, it’s possible to easily serve your multi-route React application using a static build folder.
In this tutorial, we’ll cover the steps involved in deploying such an application. To simplify things, we’ll assume that you already have a React project and a corresponding backend project with API routes configured. Throughout this tutorial, we’ll refer to these directories as blogs-frontend
and blogs-backend
respectively.
Dependencies we will use
express
— routing, middleware, and to serve static files like images, CSS and JavaScript files.cors
— will allow us to make requests from any domain. We will require and add it as middleware to our Express applicationhttp-proxy
— is a third-party Node.js module that we will use to create an HTTP proxy server that will act as an intermediary between the client and the backend server. This approach allows us to avoid CORS-related issues as well as use relative paths in our client-side code. In doing so, we can effectively hide the backend server from the client, and ensure that requests are correctly routed to the appropriate endpoint.http
— built-in Node.js module that provides an HTTP server and client. We will use it to create a web server that can make HTTP requests to other servers.path
— another built-in Node.js module that allows us to work with file and directory paths.
The inclusion of
http-proxy
in this tutorial was initially considered to allow multiple paths in the URL and act as an intermediary between the client and the backend server. This approach can be useful in more complex scenarios where you might want to proxy requests to different backend services based on the URL path. However, for the purposes of this tutorial, using a proxy server might be overcomplicating things. You can achieve the same results (avoiding CORS issues and using relative paths) by configuring your Express server correctly.
Preparing the React Application
- If you haven’t already, run
npm run build
from the root of your React project directory (i.e.,blogs-frontend
) to create a production-ready build of your React app. - Once it’s finished, navigate to your backend directory (i.e.,
blogs-backend
) and copy the build folder to your backend directory.
The way I like to do this is by adding the following script in package.json
:
"scripts": {
// ...
"build:cp": "rm -rf build && cd ../blogs-frontend && npm run build && cp -r build ../blogs-backend",
// ...
}
Then, you can simply run the following command from the terminal:
npm run build:cp
This will delete your previous build, create a new one and then copy it to your backend directory.
Setting up the Node/Express Backend
1. Create the app or server file for your backend (if you haven’t already)
From the root directory of your backend project, create a file called server.js
and install the following 3 dependencies:
npm install --save express cors http-proxy
2. Set up the Express Server
Now that we have all the dependencies we need, let’s start by requiring them and creating our Express server:
require("dotenv").config()
const express = require('express');
const path = require('path');
const cors = require("cors");
const app = express();
app.use(cors()); // Enable CORS to allow requests from any domain
app.use(express.json()); // Parse incoming JSON payloads
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
- In this example, the Express server listens on port 3001. The
express.json()
middleware is a built-in middleware in Express that parses incoming JSON payloads and makes them available inreq.body
. Without this middleware, your application would not be able to parse incoming JSON payloads and would return an error. - By calling
require("dotenv").config()
, the environment variables defined in our.env
file are loaded into theprocess.env
object and made available to your application. If you're not using environment variables, you can omit this part.
Mounting and Routing all endpoints
We will continue by mounting all of our defined API routes. Assuming we have defined our API routes in a separate folder (e.g. in a routes
folder with blogs-routes.js
and users-routes.js
), we would mount them like this:
app.use("/api/blogs", blogsRoutes);
app.use("/api/users", usersRoutes);
Then, we will instruct express to use our build
folder to serve all of our static CSS, images and JavaScript files:
app.use(express.static("build"));
Finally, we’ll add a catch-all route to handle client-side routing and serve our index.html
file. This route will render your React homepage as well as any subroutes defined within your BrowserRouter
or Switch
.
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "build", "index.html"));
});
You can use path.join
or path.resolve
, both achieve the same thing and will send the file located at blogs-backend/build/index.html
.
🚨 Keep in mind that the order in which you mount your routes/routers matters, so make sure to mount all API routes before serving your static build folder and the catch-all route to avoid conflicts.
The final code should look like this:
require("dotenv").config();
const express = require('express');
const path = require('path');
const cors = require("cors");
const blogsRoutes = require("./routes/blogs-routes");
const usersRoutes = require("./routes/users-routes");
const app = express();
app.use(cors());
app.use(express.json());
app.use("/api/blogs", blogsRoutes);
app.use("/api/users", usersRoutes);
app.use(express.static("build"));
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "build", "index.html"));
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Unknown endpoints (optional)
- You probably noticed that since the catch-all route (
app.get("*")
) matches all routes, it will handle any unknown endpoint and override any route you may have defined to handle them. - To overcome this issue, you could define separate routes for each of your React sub-routes, like this:
app.use(express.static("build"));
const indexPath = path.resolve(__dirname, "build", "index.html");
app.get("/blogs", (req, res) => res.sendFile(indexPath));
app.get("/blogs/new", (req, res) => res.sendFile(indexPath));
app.get("/blogs/:id", (req, res) => res.sendFile(indexPath));
app.get("/users", (req, res) => res.sendFile(indexPath));
app.get("/users/new", (req, res) => res.sendFile(indexPath));
app.get("/users/:id", (req, res) => res.sendFile(indexPath));
app.get("*", (req, res) => {
res.status(404).send({ error: "unknown endpoint" });
});
- By doing this, the catch-all route will handle any unknown routes after checking all of your routes (API routes and frontend routes).
- It’s also not serving
index.html
anymore in this case but simply returning a 404 response instead for all unknown endpoints and undefined React routes. - You can choose to redirect users to your homepage instead by replacing that line with
res.redirect("/")
Summary
That’s it! By following these steps, you should now have a fully functioning Node/Express server serving your static React build. By using a static build folder, you can serve your app’s assets more efficiently and reduce the load on your server.