Cookbook: single-page app + API
A React, Vue or Svelte build served with client-side routing, and an API proxied under /api on the same host.
The front end is a directory of built files; deep links like /orders/42
must get index.html (the client-side router takes over), while /api/*
goes to the backend:
# run rws in the build directory (dist/, build/)
port = 443
http_redirect_port = 80
spa_fallback = "index.html" # unknown paths get index.html…
spa_fallback_exclude_prefixes = ["/api"] # …but /api keeps its real 404s
[acme]
domains = ["app.example.com"]
email = "ops@example.com"
[[upstream]]
name = "api"
backends = ["127.0.0.1:8080"]
[upstream.health_check]
path = "/healthz"
[[route]]
name = "api"
[route.match]
path = "/api/*"
[route.action]
type = "proxy"
upstream = "api"
[route.middleware.rate_limit]
max_requests = 120
window_secs = 60Requests that match no route — the app's files and its client-side routes —
fall through to the static server. For an app under a path of a bigger site
(/cloud/ beside ordinary pages), use spa_fallback_routes = ["/cloud=cloud/index.html"] instead, so the site's other misses keep their
404s (Several apps on one server). Same origin, so no CORS settings are
needed. When the API lives on another host (api.example.com), allow the
app's origin there instead:
port = 443
[cors]
allow_origins = "https://app.example.com"
allow_methods = "GET,POST,PUT,PATCH,DELETE,OPTIONS"
allow_headers = "content-type,authorization"
allow_credentials = true
[[upstream]]
name = "api"
backends = ["127.0.0.1:8080"]
[[route]]
[route.match]
host = "api.example.com"
path = "/*"
[route.action]
type = "proxy"
upstream = "api"