Theme
Version
GitHub PyPI Discord
On this page

Serve a page with FastAPI

The components you have built so far can render without a web server. Now you will put the choice picker from the last step behind FastAPI. Its browser behavior will keep working, and Citry will gain a place to receive the server events in the next steps.

This tutorial uses FastAPI to keep the setup concrete. Citry comes with integrations for:

You can switch to your framework after you finish this journey.

Install the server packages

Inside an existing uv project, add FastAPI and Uvicorn:

uv add fastapi uvicorn

Or, with pip:

python -m pip install fastapi uvicorn

FastAPI provides the application and its routes. Uvicorn runs that application as a local web server.

Create the Citry instance

Create a new folder for this small app. Inside it, save the following 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)

The other files in this small app import the same Citry instance. That keeps its components, rendered pages, and mounted browser routes together.

The secret lets Citry detect changes to data that travels through the browser. You will use that feature when you add State.

Warning

Keep the secret outside of your source code so it does not end up in version control.

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))"

Use a stable secret from your deployment's secret store in production. All workers for the same app need the same value.

Create the page

Save this next file as components.py:

Look for the New in this step comments. Compared with the browser-only version, this file now:

  • imports the shared citry_app and assigns it to every component; and
  • exposes TutorialPage, the full page that the FastAPI route will render.
# 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">
        Choose
        <span
          class="choice-button__label"
          x-text="clientProps.label"
        ></span>
      </button>
    """

    js = """
      $component({
        props: {
          label: { type: String, required: true },
        },
        init: ({ props, scope }) => {
          scope.clientProps = props;
        },
      });
    """


class ChoicePicker(Component):
    citry = citry_app

    class Kwargs:
        pass

    class Slots:
        pass

    template = """
      <section class="choice-picker" x-data="{ choice: 'Ocean' }">
        <p>
          Current choice:
          <output
            class="choice-picker__value"
            x-text="choice"
          ></output>
        </p>

        <c-ChoiceButton
          $c-props="{ label: choice }"
          @click="choice = choice === 'Ocean' ? 'Forest' : 'Ocean'"
        />
      </section>
    """


# 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>
    """

Put the components on that instance

The complete file above repeats one new line on every component. Here is the same change with the unchanged parts folded away:

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

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

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

Component.citry tells each class which Citry instance owns it. ChoiceButton and ChoicePicker keep the browser behavior you already built. TutorialPage gives the FastAPI route one complete page to return.

Create the FastAPI app

Save this as app.py beside the other two files:

This whole file is new. Its New in this step comments mark the three connections between FastAPI and Citry.

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 = str(TutorialPage())
    return HTMLResponse(page)


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

Load the components before startup

The two local imports in the file above connect the page to the same Citry instance:

from citry_setup import citry_app
from components import TutorialPage

Importing TutorialPage runs the class definitions in components.py. Those classes register with citry_app, so they are present before the application starts serving requests.

Initialize Citry when FastAPI starts

FastAPI calls the lifespan function once when the application starts:

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


app = FastAPI(lifespan=lifespan)

citry_app.initialize() prepares the registered components before requests can arrive. The yield hands control back to FastAPI so it can run the application.

Return Citry HTML from a normal route

home is a regular FastAPI route. Inside it, we build TutorialPage, render it to a string, and returns that string as HTML:

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

A larger application can render a Citry page from its existing routes in the same way. HTMLResponse tells FastAPI and the browser that the returned string is an HTML document. Returning the plain string directly would make FastAPI encode it as a JSON string.

Mount Citry's routes

The final line gives Citry its own routes inside the FastAPI application:

mount(app, citry_app)

mount(app, citry_app) uses /citry by default. The rendered page uses those routes to load Citry's browser code. Passing the same citry_app keeps those routes connected to the components you initialized.

In the next lessons will use these routes to reach components' server-side event handlers.

Start the app

Run Uvicorn from the folder containing the three files:

uv run uvicorn app:app --reload

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

If you installed with pip, run:

python -m uvicorn app:app --reload

Visit http://127.0.0.1:8000/. You should see the choice picker inside the “Reading room” page. Click its button and watch “Ocean” change to “Forest,” just as it did without a server.

You can also visit http://127.0.0.1:8000/citry/citry.js. Seeing JavaScript at that address confirms that the Citry routes are mounted.

The Web frameworks guide shows the matching setup for Django, Flask, Starlette, and bare ASGI or WSGI apps.

Make the first server call

The page and Citry now share one running server. Next, call Python from a click.