Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 85

Serving React with an Express Server

Recommended resources

Hello! In our last lesson, you prepared your React application for production by creating a static, optimized dist build. You now have a self-contained folder with all the HTML, CSS, and JavaScript needed to run your frontend.

Today, we will bridge the gap between your frontend and backend. This lesson covers how to configure your Express server to serve that static React build while also continuing to handle all your API requests. By the end, you'll have a single, unified application that can be deployed as one unit, a standard and powerful pattern in modern web development.

The Problem: Two Separate Worlds

Throughout this course, you've been running two separate servers during development:

  • A React development server (via npm run dev on a port like 5173) that provides hot-reloading and other developer conveniences.
  • An Express backend server (via node index.js on a port like 3001) that handles API requests and database interactions.
Separated Project Structure for Node.js-Express Backend and React Frontend
This image shows a common setup where the backend and frontend are in separate directories. Our goal is to make the backend server responsible for delivering the frontend code to the user's browser.

While this separation is great for development (especially with a proxy), it's not ideal for production. Managing two different deployments can be complex and can lead to issues like Cross-Origin Resource Sharing (CORS) errors. The solution is to have your single Express server do both jobs.

Let's watch a short video that explains this problem and the goal we're trying to achieve.

Serve a React app from an Express server | React frontend and Express API setup in 1 project!

The video "Serve a React app from an Express server" by Devistry provides an excellent introduction to the problem of separate React and Express projects and outlines the benefits of combining them for deployment.

Watch from 00:00:27 to 00:04:04. The presenter clearly explains the challenges of a two-server setup (CORS, deployment complexity) and demonstrates the end goal: a single Express server that serves both the React UI and the API data.


The Solution: Serving Static Files with Express

Express has a built-in middleware function specifically for this purpose: express.static. This middleware takes a folder path as an argument and configures your server to make all the files inside that folder directly accessible to the browser.

When a request comes in, express.static will check if the URL path matches a file in the specified folder. If it does, it serves that file and the request ends. If not, it passes the request along to the next middleware in line (like your API routes).

To learn the fundamentals of this middleware, let's consult the excellent Fullstack Open course material.

Fullstack part3 | Deploying app to internet

The article "Deploying app to internet" from Fullstack Open explains exactly how to integrate a production-ready frontend with an Express backend.

Please read the sections titled "Frontend production build" and "Serving static files from the backend". Focus on the explanation of the dist directory and the core concept of using app.use(express.static('dist')).


Implementing the Full-Stack Server

Now let's put theory into practice. We need to modify our backend's main file (index.js or server.js) to serve the dist folder from your frontend project and correctly handle both frontend and API routes.

There are three key pieces to this puzzle:

  1. express.static: To serve assets like main.js and style.css.
  2. API Routes: Your existing routes (/api/users, /api/tasks, etc.) need to work as before.
  3. A "Catch-All" Route: To handle client-side routing. Any request that isn't for a static file or an API endpoint (e.g., /dashboard, /login) should serve your index.html file, letting React Router take control on the client side.

This short video provides a very clear, step-by-step walkthrough of this implementation using a Vite project, just like yours.

How to Bundle a React Frontend with an Express/Node Backend for Deployment

The video "How to Bundle a React Frontend with an Express/Node Backend for Deployment" by Arpan Neupane provides a concise and practical guide to setting this up.

Watch from 00:02:57 to 00:06:01. Pay close attention to: Running npm run build to create the dist folder. Importing the built-in path module in Node.js. The use of app.use(express.static(...)). The critical catch-all route app.get('/*', ...) that serves index.html.

The Code

Here's how you would modify your backend's main server file. Assuming your project structure is:

/full-stack-task-manager
  ├── /backend
  │   └── index.js
  └── /frontend
      └── /dist  <-- This is created by `npm run build`

You would add the following code to your backend/index.js:

const express = require('express');
const path = require('path'); // Import the 'path' module

const app = express();

// ... all your other middleware (cors, express.json, etc.)

// --- SERVE STATIC FILES ---
// This line tells Express to serve any static files from the React build directory.
// We use path.join to create an absolute path to the dist folder.
app.use(express.static(path.join(__dirname, '..', 'frontend', 'dist')));

// --- API ROUTES ---
// Your API routes should be defined here. For example:
const taskRoutes = require('./routes/taskRoutes');
app.use('/api/tasks', taskRoutes);
// ... other API routes

// --- CATCH-ALL ROUTE ---
// This catch-all route handles any requests that don't match the static files or API routes.
// It sends back the main index.html file, allowing React Router to handle the routing on the client side.
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, '..', 'frontend', 'dist', 'index.html'));
});

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Crucially, the order matters. Express processes middleware and routes in the order they are defined. The correct order is:

  1. Serve static files.
  2. Handle API routes.
  3. Serve the React app for all other routes (the catch-all).
Test your understanding!

A developer places the catch-all route app.get('*', ...) before their API routes (app.use('/api', ...)). What would be the consequence of this, and why?

Show answer

If the catch-all route comes first, it will match every incoming GET request, including those intended for the API (e.g., GET /api/tasks). As a result, instead of getting JSON data from the API, the browser would receive the index.html file for every request. This would completely break the application's ability to fetch data. Placing the more specific API routes first ensures they are handled correctly before the general catch-all is reached.


Your New Workflow

With this setup, your workflow for testing the production version locally becomes:

  1. In your frontend directory, run npm run build to create a fresh dist build.
  2. In your backend directory, run node index.js (or nodemon index.js) to start your server.
  3. Open your browser and navigate to your backend's address (e.g., http://localhost:3001).

You should see your full React application, and all API calls should work seamlessly because they are now coming from the same origin. You have successfully created a single, cohesive full-stack application!


Conclusion

You've now mastered a fundamental technique for deploying full-stack JavaScript applications. By configuring Express to serve your React build, you've created a simpler, more robust architecture that's ready for the cloud.

Key Takeaways:

  • A single Express server can be configured to serve both a static frontend and a dynamic API.
  • The express.static() middleware is used to serve the contents of your React dist (or build) folder.
  • API routes must be defined before a "catch-all" route (app.get('*', ...)) to function correctly.
  • The catch-all route is essential for single-page applications, as it ensures that client-side routes (handled by React Router) correctly load the main application shell.

Preview of the Next Lesson:
Your application is now technically ready for deployment. However, we have sensitive information like database connection strings and JWT secrets hardcoded. This is insecure and inflexible. In the next lesson, we will address this by learning how to manage environment variables for sensitive data and environment-specific settings.