[PYTHON] Display a web page with FastAPI + uvicorn + Nginx (Templates function by Jinja2)

0. Introduction

--[Jinja2](https: //) based on the one created in "FastAPI + uvicorn + nginx built with docker-compose" Create a web page template using the Templates function by jinja.palletsprojects.com/en/2.11.x/) --I've used Flask as a trial before, and I'm looking for ways to distribute Template and Static files in the same way. I wanted

Practice: Web page function added

1. Add Package

--Jinja2 for Template function requires additional installation of ʻaiofilesto deliver Static files --Reference: [Official Documents](https://fastapi.tiangolo.com/advanced/templates/#install-dependencies) --Added the following topyproject.toml`

pyproject.toml(Additions)


[tool.poetry.dependencies]
#Added the following two
jinja2 = "*"
aiofiles = "*"

In total, for example:

pyproject.toml


[tool.poetry]
name = "test_fastapi_app"
version = "0.1.0"
description = "just for test"
authors = ["Your Name <[email protected]>"]

[tool.poetry.dependencies]
python = "^3.8"
uvicorn = "*"
fastapi = "*"
jinja2 = "*"
aiofiles = "*"

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

2. Modify / add the contents of ʻapp`

--Basically, you can refer to Official example. --Here, consider the possibility of creating multi-pages or separating the functions from Rest-API functions, etc. here Try to create a web page in the form of a sub-app

File structure (ʻapp`)

$ tree
.
├── app
│   ├── Dockerfile
│   ├── app
│   │   ├── __init__.py
│   │   ├── main.py
│   │   ├── routers
│   │   │   ├── __init__.py
│   │   │   └── subpage.py
│   │   ├── static
│   │   │   ├── layout.css
│   │   │   └── subpage
│   │   │       ├── test.css
│   │   │       └── test.js
│   │   └── templates
│   │       ├── layout.html
│   │       └── subpage
│   │           └── index.html
│   ├── poetry.lock
│   └── pyproject.toml
├── docker-compose.yml
└── web

Since the files in ʻapp / app` have been changed / added, we will look at the details below.

main.py

--Corrected the contents as follows

main.py


"""                         
app main                      
"""                        
                      
import pathlib
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import RedirectResponse

from .routers import subpage

# pathlib.Use Path to get the absolute path of a static directory
PATH_STATIC = str(pathlib.Path(__file__).resolve().parent / "static")


def create_app():
    """
    create app

    -It's getting a little complicated, so it's functionalized
    """
    _app = FastAPI()

    #Sub-app of routers module`subpage`URL"/subpage/"Mount below
    _app.include_router(
        subpage.router,
        prefix="/subpage",
        tags=["subpage"],
        responses={404: {"description": "not found"}},
    )

    # static
    # URL`/static"Mount the static file below
    _app.mount(
        "/static",
        StaticFiles(directory=PATH_STATIC, html=False),
        name="static",
    )

    return _app


app = create_app()


@app.get('/')
async def redirect_subpage():
    """redirect webpage"""
    return RedirectResponse( #Redirecting to a web page created with a subpage sub-app
        "/subpage",
    )

The number of lines has increased a little, but what I'm doing is mainly

--Addition of static --Sub-app: Mount and redirect subpage

routers/subpage.py

routers/subpage.py


"""
test subpage
"""

import pathlib

from fastapi import (
    APIRouter,
    Request,
)
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse

#Get the absolute path of the templates directory
PATH_TEMPLATES = str(
    pathlib.Path(__file__).resolve() \
        .parent.parent / "templates"
)
#Jinja2 object generation
templates = Jinja2Templates(directory=PATH_TEMPLATES)


#Sub app
router = APIRouter()


@router.get("/", response_class=HTMLResponse)
async def site_root(
    request: Request,
):
    """test subpage"""
    title = "test subpage"
    return templates.TemplateResponse(
        "subpage/index.html",   # `templates`Relative path in the directory
        context={   #Variables can be passed in dict format
            "request": request,
            "title": title,
        }
    )

--Using the Template function of Jinja2, specify the file under the templates directory and return it as an HTML response. --You can pass parameters like Flask etc.

templates directory

layout.html

--Assumed to be used in common by multiple html files --Called by ʻextends` from other files

layout.html


<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
        <meta name="apple-mobile-web-app-capable" content="yes">
        {% if title %}
            <title>{{ title }}</title>
        {% else %}
            <title>Template</title>
        {% endif %}

        <!-- jQuery & Bootstrap4 -->
        <script
            src="https://code.jquery.com/jquery-3.4.1.min.js"
            integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
            crossorigin="anonymous"></script>
        <link
            rel="stylesheet"
            href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
            integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
            crossorigin="anonymous">

        <script
            src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js"
            integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
            crossorigin="anonymous"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
            integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
            crossorigin="anonymous"></script>

        <!-- jQuery UI -->
        <script
            src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"
            integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU="
            crossorigin="anonymous"></script>
        <link
            rel="stylesheet"
            type="text/css"
            href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.min.css">

        <!-- CUSTOM STYLE -->
        <link rel="stylesheet" type="text/css" href="{{url_for('static', path='/layout.css')}}">
        {% block head %}{% endblock %}

    </head>

    <body>
        {% block content %}{% endblock %}
    </body>

</html>

--Loading jQuery, jQuery-UI, Bootstrap --title is received as a parameter --The contents of the head and contents parts are individually entered in separate files. --Reading layout.css from a static directory using Jinja2's ʻurl_for` for testing

subpage/index.html

subpage/index.html


{% extends "layout.html" %}

{% block head %}
<link
    rel="stylesheet"
    type="text/css"
    href="{{ url_for('static', path='/subpage/test.css')  }}">
<script
    type="text/javascript"
    src="{{ url_for('static', path='subpage/test.js') }}"></script>
{% endblock %}


{% block content %}
<h2>Test Subpage</h2>

<br>

<h3>
    Hello, World.
</h3>

{% endblock %}

--It becomes complete HTML by ʻextends`` layout.htmland receiving the necessary parameters from the Fast API side. --Loadingtest.css and test.js` from a static directory for testing

static directory

--The details are omitted because they are only placed for testing and there is virtually no content. ――We will confirm that the contents can be read properly at the time of execution later.

3. Execution (1)

Do the following

#Package added, source modified / added, so rebuild
docker-compose build

#Service startup
docker-compose up -d

If you're running locally, take a look at http: // localhost

スクリーンショット 2020-08-16 00-28-56.png

スクリーンショット 2020-08-16 00-34-01.png

At first glance, it seems to work, but if you look closely, you can't read the static file from HTML:

<link rel="stylesheet" type="text/css" href="http://backend/static/layout.css">
        
<link
    rel="stylesheet"
    type="text/css"
    href="http://backend/static/subpage/test.css">
<script
    type="text/javascript"
    src="http://backend/static/subpage/test.js"></script>

-** I want the url of src and href to be http: // localhost / <url> in this case, but like ↑, http: // backend / <url> turn into** -** The above problem occurs when trying to use ** ʻurl_for on an HTML file --There is no problem in using ʻurl_for in ** FastAPI code ** (main.py, routers / ***. Py, etc.)

Since there is a proxy problem in this area, it is necessary to modify the startup option of ʻuvicornand the setting ofnginx` to deal with it.

4. Modify and execute settings (Part 2)

4-1. uvicorn

--If you read the official deployment and setting parts of uvicorn, you will find that It seems that the proxy-headers` option needs to be set

In conclusion, change the CMD of the Dockerfile as follows to fix the startup option of ʻuvicorn`

Dockerfile (correction)


# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--proxy-headers", "--forwarded-allow-ips", "*"]

4-2. Nginx

Next, modify the Nginx configuration file (web / conf.d / app.conf) (Refer to the example of Nginx in deployment of uvicorn)

$ tree    
.
├── app
├── docker-compose.yml
└── web
    └── conf.d
        └── app.conf

-Modify ʻapp.confin ↑ as follows --Added items inlocation /`:

conf:conf.d/app.conf


upstream backend {
    server app:8000;
}

server {
    listen 80;
    # server_name  localhost;
    # index index.html index.htm;

    location / {
        #Added the following 5 items
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;
        proxy_buffering off;

        proxy_pass http://backend;
    }

    # log
    # access_log /var/log/nginx/access.log;
    # error_log /var/log/nginx/error.log;
}

# server_tokens off;

If you do so far, the URL that you expected will be called correctly like http: // localhost / <url> even if you did ʻurl_for` on the HTML file.

image.png

-↑ The URL of the file called from static is correct

Summary

--FastAPI + uvicorn + Nginx (docker-compose) configuration to create a web function with an atmosphere like Flask ――I felt that Flask is easier to use and has a lot of literature when creating Web functions using Template functions. --FastAPI is specialized for RestAPI functions and may not be good at Web functions. However, if you can master asynchronous processing, there may be performance potential. --I also did SSL conversion (and it was quite troublesome) ~~, so I plan to write more later ~~ -→ Sequel: Display Web page with FastAPI + uvicorn + Nginx (SSL / HTTPS)

reference

-FastAPI Official Document -Starlette Official Document --FastAPI is (apparently) an extension of starlette, so you need to check the starlette specifications as appropriate. -uvicorn Official Document

Recommended Posts

Display a web page with FastAPI + uvicorn + Nginx (Templates function by Jinja2)
Display a web page with FastAPI + uvicorn + Nginx (SSL / HTTPS)
Build FastAPI + uvicorn + nginx with docker-compose
Extract data from a web page with Python
Launch a Python web application with Nginx + Gunicorn with Docker
[Django3] Display a web page in Django3 + WSL + Python virtual environment
Get a Python web page, character encode it, and display it
Display Google Maps on a web page (Spring Boot + Thymeleaf)
[python, ruby] fetch the contents of a web page with selenium-webdriver
Web App Development Practice: Create a Shift Creation Page with Django! (Shift creation page)
[FastAPI] Getting started with FastAPI, an ASGI web framework made by Python