Handling Client-Side Routing Refreshes in Node.js Express Apps
When a single-page application relies on the browser's History API, direct access or a manual refresh on a route like /products/detail results in a 404. This happens because the server receives the full path, attempts to locate a correpsonding static file or endpoint, and finds nothing. In a traditional hash-based strategy, the fragment identifier (#/products/detail) never reaches the server, so the root document always loads correctly.
To support clean URLs, you need to instruct the server to always serve the application's entry point for any request that does not match an API endpoint or a physical file.
Using a Dedicated Fallback Middleware
The connect-history-api-fallback package elegantly rewrites requests so that the main HTML file is returned where appropriate.
Install the module:
npm install connect-history-api-fallback --save
Integrate it into your Express configuration, ensuring it sits before the static file middleware:
const express = require('express');
const path = require('path');
const historyFallback = require('connect-history-api-fallback');
const app = express();
// Apply the history API fallback
app.use(historyFallback());
// Serve static assets from the build directory
app.use(express.static(path.join(__dirname, 'build')));
Customizing the Fallback File
If your entry point differs from the standard index.html, supply an index option:
app.use(historyFallback({
index: '/portal.html'
}));
Rewriting Specific URL Patterns
To redirect certain slugs to dedicated HTML files, leverage the rewrites property:
app.use(historyFallback({
rewrites: [
{
from: /^\/events/,
to: '/events-dashboard.html'
},
{
from: /^\/blog/,
to: function(context) {
return '/blog-landing.html';
}
}
]
}));
Manual Catch-All Route Approach
Alternatively, you can add a wildcard route that delivers the SPA's HTML shell. This route must be defined after all API and backend routes to avoid intercepting actual data requests.
// All backend API routes should be declared above this line.
// app.use('/api', apiRouter);
app.get('*', (request, response) => {
response.setHeader('Content-Type', 'text/html; charset=utf-8');
response.sendFile(path.resolve(__dirname, 'build', 'index.html'));
});
Important: The wildcard path should not conflict with any backend endpoint. Overlapping paths will cause the server to return raw HTML instead of expected JSON data.
Both strategies ensure that clients navigating directly to any valid frontend route receive the application bootstrap, allowing the client-side router to take over rendering seamlessly.