Connect History API Fallback Middleware
connect-history-api-fallback is a robust middleware designed for Single Page Applications (SPAs) leveraging the HTML5 History API. It addresses the common challenge where direct access to client-side routes (e.g., `/about` or `/users/123`) or page refreshes result in a 404 "Not Found" error from the web server, as these paths do not correspond to physical files. The middleware intercepts such requests, identifies those that accept `text/html` and are not direct file requests (by default, paths containing a dot), and then rewrites the request URL to a specified index file, typically `/index.html`. This allows the SPA's client-side router to take over and render the correct view. The current stable version is 2.0.0. While there isn't a strict release cadence, updates generally address compatibility, performance, or new configuration options. Key differentiators include its configurable `index` path, powerful `rewrites` option supporting both static strings and dynamic functions based on request context, and fine-grained control over accepted HTML headers and dot-file handling. It seamlessly integrates with Connect and Express-based Node.js servers.
Common errors
-
GET /some/spa/route 404 Not Found
cause The web server attempted to locate a physical file at the SPA route (e.g., `/some/spa/route`) and failed, returning a 404 error, typically on page refresh or direct navigation.fixEnsure `connect-history-api-fallback` middleware is installed and correctly configured in your server.js, placed before any generic 404 handlers, and after the first static file serving middleware. -
TypeError: history is not a function
cause The `connect-history-api-fallback` package exports a default function. This error usually occurs when attempting to destructure a named export that does not exist in CommonJS or incorrectly importing it as a named export in ESM.fixFor CommonJS, use `const history = require('connect-history-api-fallback');`. For ESM, use `import history from 'connect-history-api-fallback';`. -
Requests for static assets (e.g., /app.js, /styles.css, /logo.png) are returning index.html content or 404s.
cause Static assets with file extensions are being incorrectly rewritten by the middleware. This can be due to an incorrect `disableDotRule` setting (especially in v1), or the static file serving middleware being placed *after* the `connect-history-api-fallback` middleware.fixIn v2.0.0+, `disableDotRule` defaults to `false`, which should prevent this. For earlier versions or if you've overridden it, ensure `disableDotRule` is explicitly `false` (or unset for v2+ default behavior). Also, ensure `express.static` (or equivalent) for your static assets is called *before* `app.use(history())` in your middleware chain. -
API calls are being rewritten, e.g., 'GET /api/data' shows '302 /index.html' in logs or returns HTML content.
cause The `connect-history-api-fallback` middleware is intercepting and rewriting requests intended for your API endpoints.fixPlace your API router middleware *before* `app.use(history())`. Alternatively, use the `rewrites` option to create a rule that explicitly allows API paths to pass through (e.g., `rewrites: [{ from: /\/api\/.*$/, to: (context) => context.parsedUrl.pathname }]`).
Warnings
- breaking The `logger` option has been removed in version 2.0.0. Use the `verbose` option for console logging instead.
- breaking The default value for `disableDotRule` changed from `true` to `false` in version 2.0.0. This means that by default, URLs containing a dot (e.g., `/main.js`, `/image.png`) will *not* be rewritten to the `index.html` file. This is generally the desired behavior to allow static assets to be served directly.
- breaking The default value for `htmlAcceptHeaders` changed in version 2.0.0 from `['text/html']` to `['text/html', 'application/xhtml+xml']`.
- breaking When using the `rewrites` option, the `from` and `to` properties for each rewrite rule are now explicitly required in version 2.0.0.
- gotcha The middleware must be placed correctly in your server's middleware chain. It should come *after* your static file serving middleware (e.g., `express.static`) for actual files, but *before* any other routing logic that should be bypassed for SPA routes. Crucially, if the middleware rewrites a path to `/index.html`, another static file serving middleware placed *after* `connect-history-api-fallback` is needed to actually serve that `index.html` file.
- gotcha The `index` option specifies an *HTTP request path* (e.g., `/index.html`), not a file system path. Downstream middleware (like `express.static`) is responsible for resolving this HTTP path to an actual file on disk.
- gotcha API routes can be inadvertently rewritten to the `index.html` if they match the fallback criteria. This happens if the middleware is placed before your API router and your API paths do not contain dots or are not explicitly excluded by `rewrites`.
Install
-
npm install connect-history-api-fallback -
yarn add connect-history-api-fallback -
pnpm add connect-history-api-fallback
Imports
- history
import { history } from 'connect-history-api-fallback';import history from 'connect-history-api-fallback';
- history
const history = require('connect-history-api-fallback'); - Options
import type { Options } from 'connect-history-api-fallback';
Quickstart
const express = require('express');
const history = require('connect-history-api-fallback');
const path = require('path');
const app = express();
// Create a simple 'public' directory with an 'index.html' for this example
// You would typically have a build process generating these files.
// For demonstration, ensure you have a 'public' directory with an index.html.
// Example public/index.html content:
// <!DOCTYPE html>
// <html lang="en">
// <head><meta charset="UTF-8"><title>SPA</title></head>
// <body><h1>Welcome to SPA!</h1><p>Path: <span id="path"></span></p>
// <script>document.getElementById('path').innerText = window.location.pathname;</script>
// <nav><a href="/">Home</a> | <a href="/about">About</a> | <a href="/users/1">User</a></nav></body>
// </html>
// 1. Serve your static assets (like JS, CSS, images) first.
// This ensures that actual files are served directly and not rewritten.
app.use(express.static(path.join(__dirname, 'public')));
// 2. Use the history API fallback middleware.
// This middleware should come AFTER your static file server
// but BEFORE any other catch-all routes.
app.use(history({
// verbose: true, // Uncomment to log rewrite actions to the console
index: '/index.html', // Default in most cases, but good to be explicit
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'] // Default in v2.0.0+
}));
// 3. Serve static assets again after the history fallback.
// This is crucial: after a rewrite to `/index.html`, this `express.static` call
// will actually serve the `index.html` file from your 'public' directory.
app.use(express.static(path.join(__dirname, 'public')));
// Optional: A final catch-all for anything not handled (e.g., actual 404s for non-SPA routes)
app.get('*', (req, res) => {
res.status(404).send('Custom 404 - Resource Not Found on Server.');
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server listening on http://localhost:${PORT}`);
console.log('Navigate to /some-spa-route, then refresh the page.');
console.log('It should still serve index.html, handled by your client-side router.');
});