Browse documentation

Documentation overview

Tutorial

Reference

Project

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/.

frontend_app.pyDownload
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)),
])
frontend/index.htmlDownload
<!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.

responses_app.pyDownload
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'},
    )
Terminal
weft responses_app:app

From another terminal:

Terminal
curl -i http://127.0.0.1:8000/download
curl -N http://127.0.0.1:8000/events

FileResponse 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.