Skip to content

#236: Add a Web Interface to FastAPI

Last week we added Alembic to our to-do app to be ready for upcoming changes to the tables. In this post we extend our FastAPI application, but in an unexpected direction: we add a web interface. Let us find out how we can do that.

A plain approach

We used JSONResponse to return JSON for our objects. To return HTML, we can use the HTMLResponse class and FastAPI will take care of the content type:

1
2
3
4
5
6
from fastapi.responses import HTMLResponse

@app.get("/dashboard", include_in_schema=False)
async def dashboard():
    return HTMLResponse(content="<html><body><h1>Dashboard</h1>" +
                        "<p>12 new Task in last 7 days.</p></body></html>")

When we run this in a browser, we get back an HTML page that looks like this:

The browser shows us a minimal dashboard with 12 new tasks in the last 7 days.

The content type for this endpoint is text/html:

FastAPI sends back the dashboard as text/html.

While that works, it will be cumbersome to add full HTML pages in all endpoints. Let us explore a better way.

Choose a template engine

A template engine helps us to organise our HTML pages and let us reuse the shared parts of the layout. That way we can focus on the page-specific parts and end up with smaller HTML files.

FastAPI has a built-in support for Jinja, but we can choose whatever template engine we want. If you prefer Chameleon , you may want to use fastapi-chameleon.

I will build on my knowledge of Jinja and try to reuse the concepts from these two blog posts:

Install Jinja

We can install Jinja with this command (the package has a 2 at the end):

pip install -U jinja2

Create a template

We can create a folder template/shared and add a layout.html file based on the starter template of Bootstrap. We can put blocks where we want to inject our page-specific content:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{% block title %} To-Do Tasks {% endblock %}</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" 
        rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" 
        crossorigin="anonymous">
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
      <div class="container">
        <a class="navbar-brand" href="#">Navbar</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" 
            data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" 
            aria-expanded="false" aria-label="Toggle navigation">
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
          <ul class="navbar-nav me-auto mb-2 mb-lg-0">
            <li class="nav-item">
              <a class="nav-link" href="/dashboard">Dashboard</a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="/about">About</a>
            </li>
        </div>
      </div>
    </nav>

    <div class="container my-5">
        {% block main %} {% endblock %}
    </div>

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" 
        integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" 
        crossorigin="anonymous"></script>
    <script src="main.js"></script>
  </body>
</html>

We can now create an about.html file inside the template folder with only the part we want for the about site:

1
2
3
4
5
6
7
{% extends "./shared/layout.html" %}
{% block title %}About To-Do Task API{% endblock %}
{% block main %}
    <h1>About</h1>
    <p>This is a tiny task tracker to explain 
        Jinja templates and FastAPI</p>
{% endblock %}

The final part is the endpoint that should deliver us the right template. We need to set the BASE_DIR variable and use it in our paths, or we end up with a template not found error. The Jinja2Templates class connects our FastAPI application to the Jinja template and we can use this instance throughout our application:

...
from fastapi.templating import Jinja2Templates

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent

app = FastAPI()
app.include_router(todo.router, prefix="/api/todo")

templates = Jinja2Templates(directory=str(Path(BASE_DIR, 'templates')))

@app.get("/about", response_class=HTMLResponse)
async def about(request: Request):
    return templates.TemplateResponse(
        request=request, name="about.html"
    )

...

We can now save everything, run Uvicorn and should see an about page when we go to /about:

Our about page is put in the Bootstrap template.

Test the about page

We can put our test for the about page into the test_todo.py file and add Beautiful Soup to parse the HTML. We want to check that the title is correct and that the first H1 on the page matches "About":

1
2
3
4
5
6
7
def test_about_page():
    response = client.get("/about")
    assert response.status_code == 200

    soup = BeautifulSoup(response.text, 'html.parser')
    assert soup.title.text == "About To-Do Task API"
    assert soup.body.h1.text == "About"

Next

We explored the minimalistic approach to send HTML from our FastAPI application. To save ourselves a lot of work, we can use template engines like Jinja and put the repetitive HTML into a layout. If you look closely to the Uvicorn logs, you see a lot of 404 entries. Next week we fix that with a static route.