Serving HTML with FastAPI

Git commit: render HTML with Jinja Templates
If we are serving the content only in the form of an API, Then only the developers would be able to consume the content. Our blog should be readable by any layman without much effort. To enable this, we will have to serve HTML based response.

We will be using Jinja as our templating language. Before that, we need to make some folders and files. Notice the below folder structure of mine, the names 'apis/', 'templates/' are ending with a '/', so these are folders and others are simple .py or .html files. I have added a comment '#new' for the new files and folders that need to be created.

backend/
├─.env
├─.env.example
├─alembic/
├─alembic.ini
├─apis/
│ ├─base.py
│ ├─v1/
│ │ ├─route_blog.py
│ │ ├─route_login.py
│ │ ├─route_user.py
├─apps/
│ ├─base.py          #new
│ ├─v1/              #new
│   ├─route_blog.py  #new
├─core/
│ ├─config.py
│ ├─hashing.py
│ ├─security.py
├─db/
├─main.py
├─requirements.txt
├─schemas/
├─templates/
│ ├─base.html        #new
│ └─blog/            #new
│   └─home.html      #new
├─tests/
├─test_db.db

Now, enter the below lines in apps > v1 > route_blog.py

from fastapi import APIRouter
from fastapi import Request
from fastapi.templating import Jinja2Templates

templates = Jinja2Templates(directory="templates")
router = APIRouter()

@router.get("/")
def home(request: Request):
    return templates.TemplateResponse("blog/home.html", {"request": request})
  • We imported the necessary modules.
  • We created an object of Jinja2Templates and instantiated it with directory/folder name templates. So, now Jinja2 understands that it has to search for HTML files inside the templates folder.
  • we created an instance of APIRouter named router. But why? We could have kept all this code in the main.py file but as our codebase grows we will find it to be messy. So, we are trying to keep our codebase clean from the beginning and so, we are utilizing the APIRouter of FastAPI.
  • Next is a home function. In this function, we are basically capturing the actual request and returning an HTML response with the request in a dictionary. This dictionary is called a context dictionary. 
  • Always learn to ask why. Why are we capturing request and passing it in the context dictionary? The answer lies in request only, If we add a print statement print(dir(request)) ,we see that request has many important attributes like 'user','cookies', 'form', 'get', 'headers', 'path_params', 'query_params',  ' 'url','url_for','values' which might be used in templates. e.g. It is very common to use request.user in the template file.

Next, we need to concentrate on templates> blog > home.html. Copy the below code in this file.

{% extends "base.html" %}

{% block title %}
<title>Algoholic</title>
{% endblock %}

{% block content %}
<div class="container">
    <h1 class="display-4">All Blogs</h1>
</div>

{% endblock %}
  • I am using template inheritance here. Basically, there is some base.html file that has some empty blocks/space. We are asking Jinja to find the base.html file and insert the code in the block of home.html to block inside base.html.
  • But why am I complicating all of this? This I am doing to follow the DRY(Don't Repeat Yourself) principle. There are some common lines that we don't need to write again and again. Consider Bootstrap CDN links, we are going to use Bootstrap for all the HTML, So, why keep these links on all the pages? The navbar, and footer code are common to all the pages. So, we have base.html which will be shared with other HTML files.

This is our base.html file.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    {% block title %}
    {% endblock %}
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
  </head>
  <body>
    {% block content %}
    {% endblock %}

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
    {% block scripts %}
    {% endblock %}
  </body>
</html>

Done? No! there are still some things left. One is our main.py file has app, which does not know about all of these. So, we are going to inform our main.py file to include this router. Let's use the same pattern of keeping a base.py file that will have all the routers of apps/.

from apps.v1 import route_blog
from fastapi import APIRouter

app_router = APIRouter()

app_router.include_router(
    route_blog.router, prefix="", tags=[""], include_in_schema=False
)

Finally, we are going to include the common api_router in the main.py file.

from apis.base import api_router
from apps.base import app_router  #new

###

def include_router(app):
    app.include_router(api_router)
    app.include_router(app_router)   #new

Ok, one last thing, I promise last. We don't have Jinja2, So, add the below line in requirements.txt:

Jinja2==3.1.2

Now, install Jinja2 like with pip install -r requirements.txt. All done, not start the server with uvicorn main:app --reload and visit http://127.0.0.1:8000/. You should see a template response as:

That's all for this post, It has already become a little big! I will continue to add the navbar in the next post. 
 

FastAPITutorial

Brige the gap between Tutorial hell and Industry. We want to bring in the culture of Clean Code, Test Driven Development.

We know, we might make it hard for you but definitely worth the efforts.

Contacts

Refunds:

Refund Policy
Social

Follow us on our social media channels to stay updated.

© Copyright 2022-23 Team FastAPITutorial