Browser Application and REST API

software-engineering
full-stack
frontend
fastapi
rest
accessibility
Serve a responsive HTML/CSS/JavaScript interface and connect its session browser to typed FastAPI resource routes.

The first visible full-stack feature is session browsing. A user opens /, creates a session, selects it, and refreshes into the same state. This chapter serves committed browser assets from FastAPI, models sessions as REST resources, and treats the DOM as a projection of API data. WebSocket streaming arrives in Chapter 03 after this request/response path is stable.

Start with a complete request/response path

The browser loads index.html, app.css, and the JavaScript module from the same FastAPI origin as the API. Same-origin serving removes cross-origin configuration from the learning path and makes one process sufficient for local development. The initial JavaScript boot performs two reads: /api/config describes the selected agent mode, and /api/sessions returns session summaries.

Creating a session is resource-shaped work, so it uses POST /api/sessions. Loading one uses GET /api/sessions/{session_id}. These routes call AutocodeApplication; they do not issue SQL directly.

from tempfile import TemporaryDirectory

from fastapi.testclient import TestClient

from autocode.runner import DemoAgentRunner
from autocode_service.api import create_app

with TemporaryDirectory() as directory:
    app = create_app(
        database_path=f"{directory}/sessions.db",
        runner=DemoAgentRunner(),
        agent_mode="demo",
    )
    with TestClient(app) as client:
        page = client.get("/")
        created = client.post("/api/sessions", json={"title": "Learn the stack"})
        session_id = created.json()["session_id"]
        listed = client.get("/api/sessions").json()
        loaded = client.get(f"/api/sessions/{session_id}").json()

assert page.status_code == 200
assert created.status_code == 201
assert listed[0]["session_id"] == session_id
assert loaded["title"] == "Learn the stack"
print("browser route:", page.status_code, "session:", session_id)
browser route: 200 session: ef1f1ff2-c3b6-4b48-8a7f-beff08e51411

The test crosses the HTTP adapter and SQLite repository rather than calling route functions directly. Status 201 distinguishes creation from an ordinary read, while the returned identifier becomes the link shared by later REST and WebSocket requests. A refresh can issue the same GET and reconstruct the selected session without relying on JavaScript memory.

Let each transport match the interaction shape

REST works well when one request produces one bounded response. Session create, list, load, and event replay have that shape. An agent run does not: one submitted task produces a sequence of text deltas, tool states, and a terminal event over time. Chapter 03 uses a WebSocket for that long-lived exchange instead of forcing every browser interaction through one fashionable transport.

FastAPI generates an OpenAPI document for the REST surface. That schema is useful for inspecting paths and status codes, but it does not describe the browser’s WebSocket protocol; the course defines that command and event vocabulary separately.

from tempfile import TemporaryDirectory

from autocode_service.api import create_app

with TemporaryDirectory() as directory:
    schema = create_app(database_path=f"{directory}/sessions.db").openapi()

session_paths = {
    path: sorted(method for method in operations if method in {"get", "post"})
    for path, operations in schema["paths"].items()
    if path.startswith("/api/sessions")
}
assert session_paths["/api/sessions"] == ["get", "post"]
assert session_paths["/api/sessions/{session_id}"] == ["get"]
print("REST resource surface:", session_paths)
REST resource surface: {'/api/sessions': ['get', 'post'], '/api/sessions/{session_id}': ['get'], '/api/sessions/{session_id}/events': ['get']}

The resource names stay stable across adapters: session_id, title, version, updated_at, and the ordered events list come from the domain record. The list route intentionally returns summaries, while the detail route returns event history. This avoids loading every transcript to draw the sidebar.

Build the browser as an accessible projection

The committed frontend uses semantic landmarks, labels the composer, announces timeline changes through aria-live, and preserves keyboard-native buttons and form submission. JavaScript creates dynamic elements and assigns untrusted content through textContent. It never interpolates an agent response into innerHTML, which would turn model or repository text into executable markup.

The UI state is deliberately small: known sessions, selected session id, events keyed by event id, last cursor, socket, and streaming state. API data remains authoritative; refreshing discards in-memory state and rebuilds the projection.

from autocode_service.api import STATIC_DIR

html = (STATIC_DIR / "index.html").read_text(encoding="utf-8")
script = (STATIC_DIR / "app.js").read_text(encoding="utf-8")
style = (STATIC_DIR / "app.css").read_text(encoding="utf-8")

for required_id in ["session-list", "timeline", "composer", "message", "connection-status"]:
    assert f'id="{required_id}"' in html
assert ".textContent" in script
assert ".innerHTML" not in script
assert "@media (max-width: 760px)" in style
assert "prefers-reduced-motion" in style
print("frontend assets:", [path.name for path in sorted(STATIC_DIR.iterdir())])
frontend assets: ['app.css', 'app.js', 'index.html']

These checks are source-level guards, not a substitute for rendering the page. The chapter’s browser walkthrough verifies focus, responsive layout, empty state, session creation, and refresh behavior. Automated API tests protect the data contract; Chapter 12 adds the complete browser-to-database release check.

Exercises

Extend the session sidebar with a visible loading, empty, success, and failure state. Keep the API response as the source of truth, use DOM-safe text projection, and state what a refresh should do in every state.

[P02.1] Make a UI bug replayable

Give a sequence of reducer actions that reproduces a phantom spinner after a disconnect, then state the smallest transition that fixes it.

Back to top