Serve a frontend and other responses
Your JavaScript needs somewhere to live. Your API needs somewhere to return JSON. They can share an origin without sharing an existential crisis.
Start with one origin
The notebook serves its frontend and API together, so browser code can use fetch("/api/notes") and same-origin cookies. A frontend on port 5173 and an API on port 8000 are different origins, even on the same machine.
During development, use your frontend tool’s API proxy or configure a deliberately cross-origin API. The notebook’s auth checks expect the same-origin arrangement. Opening an HTML file through file:// is not an ingenious CORS workaround.
Use Starlette for the outer routing
Install starlette. Save this as frontend_app.py and create a sibling frontend/ directory containing the HTML below. Run weft frontend_app:app, then open http://127.0.0.1:8000/.
from pathlib import Path
from starlette.applications import Starlette
from starlette.routing import Mount
from starlette.staticfiles import StaticFiles
from wred import App
api = App()
@api.route('/hello')
async def hello(request):
return {'hello': 'world'}
app = Starlette(routes=[
Mount('/api', app=api),
Mount('/', app=StaticFiles(directory=Path(__file__).with_name('frontend'), html=True)),
])<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Wred frontend example</title>
</head>
<body>
<h1>Hello from Wred</h1>
<p id="result" role="status">Loading…</p>
<script>
async function load() {
const result = document.getElementById('result');
try {
const response = await fetch('/api/hello');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
result.textContent = JSON.stringify(await response.json());
} catch (error) {
result.textContent = `Could not load the API: ${error.message}`;
}
}
load();
</script>
</body>
</html>The outer Starlette router owns composition and lifespan. Wred registers /hello; the mount makes it /api/hello. Wred respects the ASGI root path. The final / mount catches static paths, so keep it last.
StaticFiles handles the directory. Its HTML mode can serve an index; it does not automatically make every unknown URL an SPA route. Configure an explicit fallback if your client router needs one.
Or use the notebook’s ServeStatic wrapper
The notebook already wraps its Wred app with ServeStaticASGI in example_app/app.py. You don’t need to add the Starlette router above to run that example. Both approaches delegate static files to a library built for them.
For a built frontend, Caddy or Nginx can serve the build directory and proxy /api/ to Python. Preserve the prefix when the Wred routes already include /api. Strip it only when the app is intentionally mounted with matching root-path handling. Random slash removal is a surprisingly effective 404 generator.
Return a file or stream
Wred also accepts callable ASGI response objects. This independent sample uses Starlette’s FileResponse and StreamingResponse. Save it as responses_app.py and place the sample notes.json in a sibling data/ directory.
import asyncio
from pathlib import Path
from starlette.responses import FileResponse, StreamingResponse
from wred import App
app = App()
@app.route('/download')
async def download(request):
return FileResponse(
Path(__file__).with_name('data') / 'notes.json',
filename='notes.json',
media_type='application/json',
)
@app.route('/events')
async def events(request):
async def messages():
for number in range(5):
yield f'data: {number}\n\n'
await asyncio.sleep(1)
return StreamingResponse(
messages(),
media_type='text/event-stream',
headers={'Cache-Control': 'no-cache'},
)weft responses_app:appFrom another terminal:
curl -i http://127.0.0.1:8000/download
curl -N http://127.0.0.1:8000/eventsFileResponse streams the known file and sets download headers. The events route sends five SSE messages. curl -N disables client output buffering. Authenticate and validate before starting a response; once headers are sent, an exception can’t retroactively turn it into a JSON 400.
See the Starlette response reference. Keep large downloads and long-lived streams in mind when choosing proxy buffering and timeout settings.
Borrow the notebook’s fetch habits
example_app/static/app.js has one fetch helper for JSON, auth headers, refresh, errors, and empty 204 responses. It checks response.ok: fetch resolves normally for HTTP 400 and 500, which is considerate of HTTP and occasionally inconsiderate of your expectations.
Render user text with textContent, keep credentials out of URLs, and handle empty responses before trying to parse JSON. The notebook also tracks request sequences so a slow search result cannot overwrite a newer one.
