Chapter | Title | Branch |
---|---|---|
Preface | Getting Started | main |
Chapter 1 | Using Jinja Templates to Render HTML | 01_templates |
Chapter 2 | Harnessing TailwindCSS for Consistent Design | 02_tailwindcss |
Chapter 3 | A Thin Database Layer | 03_tinydb |
Chapter 4 | Modern Browser Features Directly from HTML | 04_htmx |
This chapter focuses on how to use Jinja templates within your FastAPI application.
First things first, if you haven't already, make sure that Jinja is installed.
Note: If you're using Codespaces, you will not need to install dependencies
python -m pip install Jinja2
Create two new directories within your app
.
static
templates
Templates will create the foundation of your web application's html pages. You could create templates for each url endpoint (view), and each of these views would have access to Python objects passed from your application.
But Jinja templates allow you to nest pieces together, giving you a powerful mechanic that reduces the need to rewrite a lot of redundant html code.
For example, it is likely that the <head>
content between all your views remains the same, with the exception of maybe title or description elements.
Note: If you're coming from Django or Flask, much of this will be familiar to you. Skip ahead if you're already comfortable with template mechanics.
To get started, create a shared
folder within your templates
directory, and create a _base.html
file in the shared
folder.
Within that file, create a basic html file (it might look a little like this):
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
<title>{{ page_title }}</title>
<meta name="description" content="{{ page_description }}"/>
</head>
<body>
{% block content %}
<h1>Hello, there!</h1>
{% endblock %}
</body>
</html>
Don't worry about the {% %}
and {{ }}
just yet.
Once you have created a template, you can render it through your FastAPI route.
In your routes.py
file, create a route that renders the template you just created.
The FastAPI documentation provides instructions on how to use Jinja.
- Import Jinja2Templates.
- Create a templates object that you can re-use later.
- Declare a Request parameter in the path operation that will return a template.
- Use the templates you created to render and return a TemplateResponse, passing the request as one of the key-value pairs in the Jinja2 "context".
In practice, it looks something like this:
from fastapi import APIRouter, Request
from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="path/to/templates")
router = APIRouter()
@router.get("/")
def index(request: Request):
return templates.TemplateResponse("shared/_base.html", {"request": request})
Add this code to routes.py
. You wil need to supply the correct path to your templates directory.
Hint: It should be defined in your config file.
If you want to serve certain assets (images, css, etc...), you need to "mount" these to your FastAPI application.
Within your static
directory, create a new css
folder, and within it, create a main.css
file.
Write some basic css (this will be overwritten later). Here's an example:
html {
background-color: #00539f;
}
body {
width: 600px;
margin: 0 auto;
background-color: #ff9500;
padding: 0 20px 20px 20px;
border: 5px solid black;
}
Now, mount your static directory to your FastAPI application (in main.py
), so the content can be served.
app.mount("/static", StaticFiles(directory="path/to/static"), name="static")
Again, change the "path/to/static" to match your app structure.
Lastly, let's include the CSS file in the _base.html
template. Since it is now mounted to your app, Jinja can access it with a special method (url_for()
).
Add this line within the <head>
element in _base.html
:
<link rel="stylesheet" href="{{ url_for('static', path='css/main.css') }}" type="text/css" />
The url_for()
method looks for a static directory called static
which has already been registered with the FastAPI application, and follows the directory path starting from where it was defined in main.py
.
Note Sometimes, containers may be a little finicky with the way they treat mounted static files. In these cases, you may want to hard code the path to your static files instead [this is what is done in this repo to ensure compatibility with some of the containerization options].
The stylesheet element above would be written like this:
<link rel="stylesheet" href="/static/css/main.css" type="text/css" />
Since the "static" directory is mounted by your app (and appended to your root url path), this should still work within your application.
Lastly, in the templates
directory, create a main.html
file. This is where you can harness the real power of Jinja templates. In this file, write the following:
{% extends "/shared/_base.html" %}
{% block content %}
<h1>Simple Site</h1>
<p>This is just a simple site for you</p>
{% endblock %}
By using the {% extends "/shared/_base.html" %}
line, you are invoking the _base.html
file. But now, you have the ability to "change" anything within the {% block %}
elements.
Above, you are overwriting the {% block content %}
contained in _base.html
with new data.
To see this working, go back to the route.py
file and change the TemplateResponse
to point to main.html
instead of _base.html
.
Note: When you run your app, you should no longer see "Hello, There!" Instead, the page should render with the CSS you created earlier and the heading of "Simple Site".
I want to touch on three important items you will want to remember when working with templates.
Jinja statements are expressed with {% ... %}
Within these delimiters, you can use control structures (like "for" loops, "if/then" statements, etc..), as well as other special functions (such as the {% include ... %}
) shown above.
Expressions are contained within the {{ ... }}
delimiters.
Ordinarily, you will be passing Python objects from your FastAPI app into your template, and these objects can be accessed through these expressions. (There are also helpful filters and other neat tricks you can do within the delimiters.)
One thing to note is that you can actually pass a method object from your app and access it through these expressions. We'll see some of that later.
Above, there was a bullet point from the FastAPI documentation which talks about the "context". Here is that point again:
- Use the templates you created to render and return a TemplateResponse, passing the request as one of the key-value pairs in the Jinja2 "context".
The "context" here is very important. Think of it as the data that you want to pass from your application to your templates as key:value pairs. These objects can then be accessed through the Jinja Expressions.
For example, look at this template response:
@router.get("/")
def index(request: Request):
return templates.TemplateResponse(
"shared/_base.html",
{
"request": request,
"age": 42
}
)
The dictionary that includes the request
and age
keys is considered the "context". Per the FastAPI docs, you always have to send the request
as part of the context. In addition, we are passing the age
key.
In a Jinja template, you can then access the value of age
using an expression.
<p>The answer is {{ age }}.<p>
Which would ultimately be rendered as:
The answer is 42.
There's much more to Jinja templates, but it's vital to understand those three key points.
You can run your app at any time to see if everything is working as intended. However, uvicorn
can watch for changes in your app and restart the server automatically, so you don't have to keep running the command.
uvicorn app.main:app --reload
The --reload
option restarts the server every time a change is made (and saved) to a .py
file, so you can see changes reflected in your browser right away.
If the command is successful, you can visit 127.0.0.1:8000 to see your app in action.
If you change/save a .py
file in your app, the server will restart automatically.
Note: The only files that are "watched" by default are
.py
files. If you want to restart the server when changes are saved to other file types, you can use the--reload-include TEXT
option. The TEXT is a glob pattern.To include html files, for example, you would use the following command:
uvicorn app.main:app --reload --reload-include *.html
Take some time to flesh out your templates. You can start by including <meta>
tags to your _base.html
file. Think about which of those tags may need to have dynamic data, such as the <title>
tag. How could you display a different title for each page that a user might visit?
For reference, here are some common meta tags:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
<title>Simple Site</title>
<meta name="description" content="Built with TailwindCSS and htmx"/>
<!-- Facebok Open Graph: https://developers.facebook.com/docs/sharing/opengraph -->
<meta property="og:site_name" content="Simple Site"/>
<meta property="og:type" content="Demo site for TailwindCSS and htmx"/>
<meta property="og:title" content="Simple Site - Home"/>
<meta property="og:description" content="Look, ma! No JavaScript!"/>
<meta property="og:url" content="https://wwww.example.com"/>
<!-- Favicon! -->
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', path='favicon/apple-touch-icon.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', path='favicon/favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', path='favicon/favicon-16x16.png') }}">
<link rel="manifest" href="/site.webmanifest">
<!-- CSS -->
<link rel="stylesheet" href="{{ url_for('static', path='css/main.css') }}" type="text/css" />
</head>
Note: Favicons are great! Why not use a generator and create some for your project? Where will you put them?
Once you've had your fun with templates, it's time to move on to TailwindCSS!