# Local SPA Proxy Server Pattern When testing a React/Vite SPA locally that needs to reach a backend API on a different port (e.g., PocketBase on 8091, or any API server), use a Python HTTP server that serves static files AND proxies API paths to the real backend. ## Problem SPAs built with Vite often reference backend APIs at relative paths like `/pb` or `/api`. When served from a simple `python3 -m http.server`, these requests go to the wrong origin and fail with 404 or CORS errors. ## Solution A ~60-line Python script that: 1. Serves static files from the `dist/` directory 2. Proxies API paths (`/pb/*`, `/api/*`) to the real backend 3. Adds CORS headers to all responses 4. Handles SPA fallback (serves `index.html` for unknown paths) 5. Supports GET, POST, PUT, PATCH, DELETE, OPTIONS methods ## Template Save as `serve-proxy.py` in the project root: ```python #!/usr/bin/env python3 """Serve SPA dist + proxy /pb to backend.""" from http.server import HTTPServer, SimpleHTTPRequestHandler import urllib.request, os DIST = './dist' BACKEND = 'http://127.0.0.1:8091' class Handler(SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): super().__init__(*args, directory=DIST, **kwargs) def end_headers(self): self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS') self.send_header('Access-Control-Allow-Headers', 'Authorization, Content-Type') super().end_headers() def do_OPTIONS(self): self.send_response(204) self.end_headers() def do_GET(self): if self.path.startswith('/pb') or self.path.startswith('/api'): self.proxy() else: full = os.path.join(DIST, self.path.lstrip('/')) if not os.path.exists(full) or os.path.isdir(full) and self.path != '/': self.path = '/index.html' super().do_GET() def do_POST(self): self.proxy() if self.is_api_path() else self.send_error(405) def do_PUT(self): self.proxy() def do_PATCH(self): self.proxy() def do_DELETE(self): self.proxy() def is_api_path(self): return self.path.startswith('/pb') or self.path.startswith('/api') def proxy(self): url = BACKEND + self.path # Strip proxy prefix if the SDK already includes it if self.path.startswith('/pb'): url = BACKEND + self.path[3:] if not url.startswith(BACKEND + '/api'): url = BACKEND + '/api' + self.path[3:] body = None if self.headers.get('Content-Length'): body = self.rfile.read(int(self.headers['Content-Length'])) req = urllib.request.Request(url, data=body, method=self.command) for k, v in self.headers.items(): if k.lower() not in ('host', 'connection', 'origin', 'referer'): req.add_header(k, v) try: resp = urllib.request.urlopen(req) self.send_response(resp.status) for k, v in resp.headers.items(): if k.lower() not in ('transfer-encoding', 'connection'): self.send_header(k, v) self.end_headers() self.wfile.write(resp.read()) except urllib.error.HTTPError as e: self.send_response(e.code) self.end_headers() self.wfile.write(e.read()) if __name__ == '__main__': server = HTTPServer(('0.0.0.0', 4173), Handler) print(f'Serving on http://0.0.0.0:4173 → {BACKEND}') server.serve_forever() ``` ## Common Pitfalls ### Double `/api` prefix The PocketBase JS SDK constructs paths like `/pb/api/collections/...`. The proxy must strip `/pb` and append the remainder, NOT add another `/api/`. Check what paths the SDK actually builds by inspecting the built JS or testing with curl. ### PocketBase SDK adds `/api/` to the base URL `new PocketBase('/pb')` builds URLs as: `/pb/api/collections/...`. The SDK always inserts `/api/` between the base URL and the collection path. ### Missing CORS on error responses If `urllib.error.HTTPError` is caught, the error response body is written but CORS headers from `end_headers()` may not be called in the right order. Ensure `end_headers()` is called before `wfile.write()` in error paths. ### Empty browser console errors React lazy-loaded chunk failures often produce empty exceptions in browser consoles. If a page renders blank with no visible error, revert to eager imports temporarily to surface the actual error message. ## Usage ```bash # Start server in background python3 serve-proxy.py & # Access at http://localhost:4173 or http://:4173 ``` ## When to Use - Testing a local SPA build that needs API access - Debugging frontend-backend integration without nginx reverse proxy - Quick demo serving with `python3 -m http.server` + CORS + API proxy