Single page apps
Single Page Applications (SPAs) handle routing on the client side, which requires special server configuration. When users navigate to a route like /dashboard or /settings, the browser requests that path from the server. Since these aren't real files, the server needs to return the main index.html file so the client-side router can handle the route.
Serverpod provides SpaRoute to handle this pattern automatically.
Basic setup
Use SpaRoute to serve your SPA with automatic fallback to index.html:
final webDir = Directory('web/app');
pod.webServer.addRoute(
SpaRoute(
webDir,
fallback: File('web/app/index.html'),
),
'/**',
);
This configuration:
- Serves static files from
web/appwhen they exist - Falls back to
index.htmlfor any path that doesn't match a file - Enables client-side routing frameworks (React Router, Vue Router, etc.) to work correctly
How it works
When a request comes in:
SpaRoutefirst tries to serve a matching static file from the directory- If no file exists (404 response), it serves the fallback file instead
- The client-side JavaScript then handles routing based on the URL
This is implemented using FallbackMiddleware internally, which you can also use directly for custom fallback behavior.
Cache control
Configure caching for your static assets:
pod.webServer.addRoute(
SpaRoute(
webDir,
fallback: File('web/app/index.html'),
cacheControlFactory: StaticRoute.publicImmutable(
maxAge: const Duration(minutes: 5),
),
),
'/**',
);
See Static Files for more on cache control.
Cache busting
Enable cache-busted URLs for your assets:
final webDir = Directory('web/app');
final cacheBustingConfig = CacheBustingConfig(
mountPrefix: '/',
fileSystemRoot: webDir,
);
pod.webServer.addRoute(
SpaRoute(
webDir,
fallback: File('web/app/index.html'),
cacheBustingConfig: cacheBustingConfig,
cacheControlFactory: StaticRoute.publicImmutable(
maxAge: const Duration(minutes: 5),
),
),
'/**',
);
See Static Files for more on cache busting.
Using FallbackMiddleware directly
For more control, use FallbackMiddleware with StaticRoute:
final webDir = Directory('web/app');
final indexFile = File('web/app/index.html');
pod.webServer.addMiddleware(
FallbackMiddleware(
fallback: StaticRoute.file(indexFile),
on: (response) => response.statusCode == 404,
),
'/**',
);
pod.webServer.addRoute(StaticRoute.directory(webDir), '/**');
This gives you flexibility to customize the fallback condition. For example, you could fall back on any 4xx error:
FallbackMiddleware(
fallback: StaticRoute.file(indexFile),
on: (response) => response.statusCode >= 400 && response.statusCode < 500,
)
Serving Flutter web applications
For serving Flutter web applications specifically, see Flutter web apps.