Skip to content

Commit c9aab72

Browse files
authored
docs: Add documentation for serving Flutter web applications with Serverpod (#359)
1 parent dca8f4c commit c9aab72

File tree

4 files changed

+274
-2
lines changed

4 files changed

+274
-2
lines changed

docs/06-concepts/18-webserver/01-overview.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,5 @@ class UserRoute extends Route {
152152
- **[Middleware](middleware)** - Intercept and transform requests and responses
153153
- **[Static Files](static-files)** - Serve static assets
154154
- **[Server-side HTML](server-side-html)** - Render HTML dynamically on the server
155+
- **[Single Page Apps](single-page-apps)** - Serve SPAs with client-side routing
156+
- **[Flutter Web Apps](flutter-web)** - Serve Flutter web applications

docs/06-concepts/18-webserver/05-static-files.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Control how browsers and CDNs cache your static files using the
3737
pod.webServer.addRoute(
3838
StaticRoute.directory(
3939
staticDir,
40-
cacheControlFactory: StaticRoute.publicImmutable(maxAge: const Duration(years: 1)),
40+
cacheControlFactory: StaticRoute.publicImmutable(maxAge: const Duration(minutes: 5)),
4141
),
4242
'/static/**',
4343
);
@@ -65,7 +65,7 @@ pod.webServer.addRoute(
6565
StaticRoute.directory(
6666
staticDir,
6767
cacheBustingConfig: cacheBustingConfig,
68-
cacheControlFactory: StaticRoute.publicImmutable(maxAge: const Duration(years: 1)),
68+
cacheControlFactory: StaticRoute.publicImmutable(maxAge: const Duration(minutes: 5)),
6969
),
7070
'/static/**',
7171
);
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Single page apps
2+
3+
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.
4+
5+
Serverpod provides `SpaRoute` to handle this pattern automatically.
6+
7+
## Basic setup
8+
9+
Use `SpaRoute` to serve your SPA with automatic fallback to `index.html`:
10+
11+
```dart
12+
final webDir = Directory('web/app');
13+
14+
pod.webServer.addRoute(
15+
SpaRoute(
16+
webDir,
17+
fallback: File('web/app/index.html'),
18+
),
19+
'/**',
20+
);
21+
```
22+
23+
This configuration:
24+
25+
- Serves static files from `web/app` when they exist
26+
- Falls back to `index.html` for any path that doesn't match a file
27+
- Enables client-side routing frameworks (React Router, Vue Router, etc.) to work correctly
28+
29+
## How it works
30+
31+
When a request comes in:
32+
33+
1. `SpaRoute` first tries to serve a matching static file from the directory
34+
2. If no file exists (404 response), it serves the fallback file instead
35+
3. The client-side JavaScript then handles routing based on the URL
36+
37+
This is implemented using `FallbackMiddleware` internally, which you can also use directly for custom fallback behavior.
38+
39+
## Cache control
40+
41+
Configure caching for your static assets:
42+
43+
```dart
44+
pod.webServer.addRoute(
45+
SpaRoute(
46+
webDir,
47+
fallback: File('web/app/index.html'),
48+
cacheControlFactory: StaticRoute.publicImmutable(
49+
maxAge: const Duration(minutes: 5),
50+
),
51+
),
52+
'/**',
53+
);
54+
```
55+
56+
See [Static Files](static-files#cache-control) for more on cache control.
57+
58+
## Cache busting
59+
60+
Enable cache-busted URLs for your assets:
61+
62+
```dart
63+
final webDir = Directory('web/app');
64+
65+
final cacheBustingConfig = CacheBustingConfig(
66+
mountPrefix: '/',
67+
fileSystemRoot: webDir,
68+
);
69+
70+
pod.webServer.addRoute(
71+
SpaRoute(
72+
webDir,
73+
fallback: File('web/app/index.html'),
74+
cacheBustingConfig: cacheBustingConfig,
75+
cacheControlFactory: StaticRoute.publicImmutable(
76+
maxAge: const Duration(minutes: 5),
77+
),
78+
),
79+
'/**',
80+
);
81+
```
82+
83+
See [Static Files](static-files#static-file-cache-busting) for more on cache busting.
84+
85+
## Using FallbackMiddleware directly
86+
87+
For more control, use `FallbackMiddleware` with `StaticRoute`:
88+
89+
```dart
90+
final webDir = Directory('web/app');
91+
final indexFile = File('web/app/index.html');
92+
93+
pod.webServer.addMiddleware(
94+
FallbackMiddleware(
95+
fallback: StaticRoute.file(indexFile),
96+
on: (response) => response.statusCode == 404,
97+
),
98+
'/**',
99+
);
100+
101+
pod.webServer.addRoute(StaticRoute.directory(webDir), '/**');
102+
```
103+
104+
This gives you flexibility to customize the fallback condition. For example, you could fall back on any 4xx error:
105+
106+
```dart
107+
FallbackMiddleware(
108+
fallback: StaticRoute.file(indexFile),
109+
on: (response) => response.statusCode >= 400 && response.statusCode < 500,
110+
)
111+
```
112+
113+
## Serving Flutter web applications
114+
115+
For serving Flutter web applications specifically, see [Flutter web apps](flutter-web).
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Flutter web apps
2+
3+
Serverpod can serve your Flutter web application directly, allowing you to host both your API and web frontend from the same server. `FlutterRoute` handles the specifics of serving Flutter web apps, including WASM multi-threading headers and SPA-style routing.
4+
5+
## Basic setup
6+
7+
Use `FlutterRoute` to serve your Flutter web build:
8+
9+
```dart
10+
pod.webServer.addRoute(
11+
FlutterRoute(Directory('web/app')),
12+
'/**',
13+
);
14+
```
15+
16+
This configuration:
17+
18+
- Serves all static files from the Flutter build
19+
- Falls back to `index.html` for client-side routing
20+
- Adds WASM multi-threading headers automatically
21+
22+
## Building Flutter for web
23+
24+
Build your Flutter app for web with WASM support for improved performance and multi-threading:
25+
26+
```bash
27+
cd my_project_flutter
28+
flutter build web --wasm
29+
```
30+
31+
:::info
32+
33+
WASM builds automatically fall back to JavaScript in browsers that don't support WebAssembly Garbage Collection (WasmGC). Your app works everywhere while taking advantage of WASM performance where available.
34+
35+
:::
36+
37+
## Project structure
38+
39+
Copy your Flutter build output to the server's `web` directory:
40+
41+
```text
42+
my_project/
43+
├── my_project_server/
44+
│ ├── lib/
45+
│ │ └── server.dart
46+
│ └── web/
47+
│ └── app/ # Flutter web build output
48+
│ ├── index.html
49+
│ ├── main.dart.js
50+
│ ├── flutter.js
51+
│ └── ...
52+
├── my_project_flutter/
53+
│ └── build/
54+
│ └── web/ # Flutter build output (source)
55+
└── my_project_client/
56+
```
57+
58+
## WASM multi-threading
59+
60+
Flutter WASM builds can use multi-threaded rendering for improved performance. This requires `SharedArrayBuffer`, which browsers only enable with specific security headers.
61+
62+
`FlutterRoute` automatically adds these headers:
63+
64+
- `Cross-Origin-Opener-Policy: same-origin`
65+
- `Cross-Origin-Embedder-Policy: require-corp`
66+
67+
### Using WasmHeadersMiddleware directly
68+
69+
If you're using `SpaRoute` or custom routes instead of `FlutterRoute`, add the headers manually with `WasmHeadersMiddleware`:
70+
71+
```dart
72+
pod.webServer.addMiddleware(const WasmHeadersMiddleware(), '/**');
73+
74+
pod.webServer.addRoute(
75+
SpaRoute(
76+
Directory('web/app'),
77+
fallback: File('web/app/index.html'),
78+
),
79+
'/**',
80+
);
81+
```
82+
83+
## Cache control
84+
85+
Configure caching for Flutter's static assets:
86+
87+
```dart
88+
pod.webServer.addRoute(
89+
FlutterRoute(
90+
Directory('web/app'),
91+
cacheControlFactory: StaticRoute.publicImmutable(
92+
maxAge: const Duration(minutes: 5),
93+
),
94+
),
95+
'/**',
96+
);
97+
```
98+
99+
See [Static Files](static-files#cache-control) for more on cache control.
100+
101+
## Cache busting
102+
103+
Enable cache-busted URLs:
104+
105+
```dart
106+
final webDir = Directory('web/app');
107+
108+
final cacheBustingConfig = CacheBustingConfig(
109+
mountPrefix: '/',
110+
fileSystemRoot: webDir,
111+
);
112+
113+
pod.webServer.addRoute(
114+
FlutterRoute(
115+
webDir,
116+
cacheBustingConfig: cacheBustingConfig,
117+
cacheControlFactory: StaticRoute.publicImmutable(
118+
maxAge: const Duration(minutes: 5),
119+
),
120+
),
121+
'/**',
122+
);
123+
```
124+
125+
See [Static Files](static-files#static-file-cache-busting) for more on cache busting.
126+
127+
## Complete example
128+
129+
Here's a complete `server.dart` serving a Flutter web app:
130+
131+
```dart
132+
import 'dart:io';
133+
134+
import 'package:serverpod/serverpod.dart';
135+
136+
import 'src/generated/protocol.dart';
137+
import 'src/generated/endpoints.dart';
138+
139+
void run(List<String> args) async {
140+
final pod = Serverpod(args, Protocol(), Endpoints());
141+
142+
final flutterAppDir = Directory('web/app');
143+
144+
if (!flutterAppDir.existsSync()) {
145+
print('Warning: Flutter web app not found at ${flutterAppDir.path}');
146+
print('Build your Flutter app and copy it to web/app/');
147+
} else {
148+
pod.webServer.addRoute(FlutterRoute(flutterAppDir), '/**');
149+
}
150+
151+
await pod.start();
152+
}
153+
```
154+
155+
With this configuration, your Flutter web app is served at the root URL of your web server (typically `http://localhost:8082` in development).

0 commit comments

Comments
 (0)