Theme
Version
GitHub PyPI Discord
On this page

Serve pages with FastAPI

So far you rendered components to a file. A real app serves pages from a web server, and a server is also what lets a click in the browser run Python. In this step you serve the choice picker from the last step with FastAPI. Its browser behavior keeps working, and Citry gets the routes it needs for the server events in the next steps.

This tutorial uses FastAPI to keep the setup concrete. Citry also works with:

You can switch to your framework after you finish the tutorial.

Install FastAPI

FastAPI defines the application and its routes. Uvicorn runs that application as a local web server. Inside an existing uv project, add both:

uv add fastapi uvicorn

Or, with pip:

python -m pip install fastapi uvicorn

Set up Citry

A Citry instance holds your app's components and settings. Create a new folder for this small app, and save this inside it as citry_setup.py:

import os

from citry import Citry

# New in this step: create one Citry instance for the whole app.
secret = os.environ.get("CITRY_SECRET")
if not secret:
    msg = "Set CITRY_SECRET before starting the app."
    raise RuntimeError(msg)

citry_app = Citry(secret=secret)

Every other file in the app imports this same Citry instance, so the components, the rendered pages, and Citry's browser routes all use the same settings.

The secret lets Citry detect when someone changes data that passes through the browser. Citry uses it once you add State in a later step, but citry_setup.py stops with an error when the secret is missing, so set it now.

Create a random development secret in your current terminal:

export CITRY_SECRET="$(
  python -c 'import secrets; print(secrets.token_urlsafe(32))'
)"

In PowerShell, use:

$env:CITRY_SECRET = python -c "import secrets; print(secrets.token_urlsafe(32))"

Keep the secret out of your source code

Read the secret from the environment so it never ends up in version control. In production, use a stable secret from your deployment's secret store, and give every worker of the app the same value.

Create the page

Save this as components.py:

# New in this step: every component uses the app's Citry instance.
from citry_setup import citry_app

from citry import Component

class ChoiceButton(Component):
    citry = citry_app

    class Kwargs:
        pass

    class Slots:
        pass

    template = """
      <button
        class="choice-button"
        type="button"
        @click="$emit('select')"
      >
        Choose
        <span
          class="choice-button__label"
          v-text="label"
        ></span>
      </button>
    """

    js = """
      $component({
        props: {
          label: { type: String, required: true },
        },
        emits: ['select'],
      });
    """

class ChoicePicker(Component):
    citry = citry_app

    class Kwargs:
        pass

    class Slots:
        pass

    template = """
      <section class="choice-picker">
        <p>
          Current choice:
          <output
            class="choice-picker__value"
            v-text="choice"
          ></output>
        </p>

        <c-ChoiceButton
          :label="choice"
          @select="toggleChoice"
        />
      </section>
    """

    js = """
      $component({
        data() {
          return { choice: 'Ocean' };
        },
        methods: {
          toggleChoice() {
            this.choice =
              this.choice === 'Ocean' ? 'Forest' : 'Ocean';
          },
        },
      });
    """

# New in this step: FastAPI will return this complete page.
class TutorialPage(Component):
    citry = citry_app

    class Kwargs:
        pass

    class Slots:
        pass

    template = """
      <!DOCTYPE html>
      <html lang="en">
        <head>
          <meta charset="utf-8" />
          <title>Reading room</title>
        </head>
        <body>
          <main>
            <h1>Reading room</h1>
            <c-ChoicePicker />
          </main>
        </body>
      </html>
    """

The New in this step comments mark what changed from the browser-only version. Each existing component gets one new line:

class ChoiceButton(Component):
    citry = citry_app
    ...

Component.citry tells the class which Citry instance it belongs to. The new TutorialPage component is the complete HTML page that FastAPI will return.

Create the FastAPI app

Save this as app.py beside the other two files. The whole file is new, and its New in this step comments mark the three places where FastAPI and Citry meet.

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from citry_setup import citry_app
from components import TutorialPage
from fastapi import FastAPI
from fastapi.responses import HTMLResponse

from citry.contrib.fastapi import mount

# New in this step: initialize Citry before serving requests.
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
    citry_app.initialize()
    yield

app = FastAPI(lifespan=lifespan)

# New in this step: render a Citry page from a regular route.
@app.get("/")
def home() -> HTMLResponse:
    page = TutorialPage()
    html = str(page)
    return HTMLResponse(html)

# New in this step: add Citry's browser and event routes.
mount(app, citry_app)

The next three sections walk through those places.

Initialize at startup

@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
    citry_app.initialize()
    yield

app = FastAPI(lifespan=lifespan)

FastAPI runs this lifespan function once when the app starts. citry_app.initialize() gets the components ready before the first request arrives. The yield hands control back to FastAPI so it can serve requests.

Importing TutorialPage at the top of app.py runs the class definitions in components.py, so every component already belongs to citry_app when initialize() runs.

Return the page

home is an ordinary FastAPI route. It renders TutorialPage to a string and returns it as HTML:

@app.get("/")
def home() -> HTMLResponse:
    page = TutorialPage()
    html = str(page)
    return HTMLResponse(html)

HTMLResponse tells FastAPI and the browser that the string is an HTML document. If you returned the plain string, FastAPI would send it as JSON. Your existing routes can return Citry pages the same way.

Add Citry's routes

The last line adds Citry's own routes to the FastAPI app:

mount(app, citry_app)

mount(app, citry_app) puts them under /citry by default. The page loads Citry's browser code from these routes, and in the next steps, clicks use them to call Python.

Start the app

Run Uvicorn from the folder that holds the three files:

uv run uvicorn app:app --reload

If you installed with pip, run:

python -m uvicorn app:app --reload

In app:app, the first app is the file app.py, and the second is the FastAPI object inside it. --reload restarts the server when you save a change.

Visit http://127.0.0.1:8000/. You see the choice picker on the β€œReading room” page. Click its button, and β€œOcean” changes to β€œForest,” just as it did without a server.

To confirm that Citry's routes work, visit http://127.0.0.1:8000/citry/citry.js. You should see JavaScript, Citry's browser code.

Next steps

The page and Citry now run on one server. Next, call Python from a click.

To use another framework, the Web frameworks guide shows the matching setup for Django, Flask, Starlette, and plain ASGI or WSGI apps.