pythonhtmljinja2fastapi

How to submit HTML form <input> value using FastAPI and Jinja2 Templates?


I am facing the following issue while trying to pass a value from an HTML form <input> element to the form's action attribute and send it to the FastAPI server.

This is how the Jinja2 (HTML) template is loaded:

# Test TEMPLATES
@app.get("/test",response_class=HTMLResponse)
async def read_item(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

My HTML form:

<form action="/disableSubCategory/{{subCatName}}">
    <label for="subCatName">SubCategory:</label><br>
    <input type="text" id="subCatName" name="subCatName" value=""><br>
    <input type="submit" value="Disable">
</form>

My FastAPI endpoint to be called in the form action:

# Disable SubCategory
@app.get("/disableSubCategory/{subCatName}")
async def deactivateSubCategory(subCatName: str):
    disableSubCategory(subCatName)
    return {"message": "SubCategory [" + subCatName + "] Disabled"}

The error I get:

"GET /disableSubCategory/?subCatName=Barber HTTP/1.1" 404 Not Found

What I am trying to achieve is the following FastAPI call:

/disableSubCategory/{subCatName} ==> "/disableSubCategory/Barber"

Anyone who could help me understand what I am doing wrong?

Thanks. Leo


Solution

  • Option 1

    You could have the category name defined as Form parameter in the backend, and submit a POST request from the frontend using an HTML <form>, as described in Method 1 of this answer.

    app.py

    from fastapi import FastAPI, Form, Request
    from fastapi.responses import HTMLResponse
    from fastapi.templating import Jinja2Templates
    
    app = FastAPI()
    templates = Jinja2Templates(directory='templates')
    
    @app.post('/disable')
    async def disable_cat(cat_name: str = Form(...)):
        return f'{cat_name} category has been disabled.'
    
    @app.get('/', response_class=HTMLResponse)
    async def main(request: Request):
        return templates.TemplateResponse('index.html', {'request': request})
    

    templates/index.html

    <!DOCTYPE html>
    <html>
       <head>
          <meta charset="utf-8">
          <meta name="viewport" content="width=device-width, initial-scale=1">
       </head>
       <body>
          <h1>Disable a category</h1>
          <form method="post" action="/disable">
             <label for="cat_name">Enter a category name to disable:</label><br>
             <input type="text" id="cat_name" name="cat_name">
             <input class="submit" type="submit" value="Submit">
          </form>
       </body>
    </html>
    

    To achieve the same result, i.e., submiting Form data to the backend through a POST request, using JavaScript's Fetch API instead, you could use the following template in the frontend (see Option 4 later on in this answer for a similar approach):

    templates/index.html

    <!DOCTYPE html>
    <html>
       <head>
          <meta charset="utf-8">
          <meta name="viewport" content="width=device-width, initial-scale=1">
       </head>
       <body>
          <h1>Disable a category</h1>
          <label for="cat_name">Enter a category name to disable:</label><br>
          <input type="text" id="cat_name" name="cat_name">
          <input type="button" value="Submit" onclick="send()">
          <p id="resp"></p>
          <script>
             function send() {
                var resp = document.getElementById("resp");
                const cat_name = document.getElementById("cat_name").value;
                var formData = new FormData();
                formData.append("cat_name", cat_name);
             
                fetch('/disable', {
                      method: 'POST',
                      body: formData,
                   })
                   .then(response => response.json())
                   .then(data => {
                      resp.innerHTML = JSON.stringify(data); // data is a JSON object
                   })
                   .catch(error => {
                      console.error(error);
                   });
             }
          </script>
       </body>
    </html>
    

    To use arbitrary Form keys, i.e., if the Form keys/names are not known beforehand to the backend, you might want to have a look at this answer.

    Option 2

    You could have the category name declared as query parameter in your endpoint, and in the frontend use a similar approach to the one demonstrated in your question to convert the value from the <form> <input> element into a query parameter, and then add it to the query string of the URL (in the action attribute).

    Note that the below uses a GET request in contrast to the above (in this case, you need to use @app.get() in the backend and <form method="get" ... in the frontend, which is the default method anyway). Beware that most browsers cache GET requests (i.e., saved in browser's history), thus making them less secure compared to POST, as the data sent are part of the URL and visible to anyone who has access to the device. Thus, GET method should not be used when sending passwords or other sensitive information.

    app.py

    from fastapi import FastAPI, Request
    from fastapi.responses import HTMLResponse
    from fastapi.templating import Jinja2Templates
    
    app = FastAPI()
    templates = Jinja2Templates(directory='templates')
    
    @app.get('/disable')
    async def disable_cat(cat_name: str):
        return f'{cat_name} category has been disabled.'
    
    @app.get('/', response_class=HTMLResponse)
    async def main(request: Request):
        return templates.TemplateResponse('index.html', {'request': request})
    

    templates/index.html

    <!DOCTYPE html>
    <html>
       <head>
          <meta charset="utf-8">
          <meta name="viewport" content="width=device-width, initial-scale=1">
       </head>
       <body>
          <h1>Disable a category</h1>
          <form method="get" id="myForm" action='/disable{{ cat_name }}'>
             <label for="cat_name">Enter a category name to disable:</label><br>
             <input type="text" id="cat_name" name="cat_name">
             <input class="submit" type="submit" value="Submit">
          </form>
       </body>
    </html>
    

    If you instead would like to use a POST request—which might make more sense when updating content/state on the server compared to GET that should be used when requesting (not modifying) data—you could define the FastAPI endpoint in app.py above with @app.post(), as well as replace the above template with the one below (similar to Method 2 of this answer), which submits the <form> using POST method, after transforming the <form> data into query parameters. Note that since the data are still sent as part of the query string, one would still be able to see them in the browser's history, despite of using a POST request method; hence, in this case, using a POST method, it doesn't actually make it a more secure way of transferring those data. To avoid that, one should submit the data in the request body instead, as demonstrated in Option 1 earlier.

    <!DOCTYPE html>
    <html>
       <head>
          <meta charset="utf-8">
          <meta name="viewport" content="width=device-width, initial-scale=1">
          <script>
             document.addEventListener('DOMContentLoaded', (event) => {
                document.getElementById("myForm").addEventListener("submit", function (e) {
                   var myForm = document.getElementById('myForm');
                   var qs = new URLSearchParams(new FormData(myForm)).toString();
                   myForm.action = '/disable?' + qs;
                });
             });
          </script>
       </head>
       <body>
          <h1>Disable a category</h1>
          <form method="post" id="myForm">
             <label for="cat_name">Enter a category name to disable:</label><br>
             <input type="text" id="cat_name" name="cat_name">
             <input class="submit" type="submit" value="Submit">
          </form>
       </body>
    </html>
    

    Option 3

    You could still have it defined as path parameter (as shown in your question), and use JavaScript in the frontend to modify the action attribute of the HTML <form>, by passing the value of the <form> <input> element as path parameter to the URL, similar to what has been described earlier.

    app.py

    from fastapi import FastAPI, Request
    from fastapi.responses import HTMLResponse
    from fastapi.templating import Jinja2Templates
    
    app = FastAPI()
    templates = Jinja2Templates(directory='templates')
    
    @app.post('/disable/{name}')
    async def disable_cat(name: str):
        return f'{name} category has been disabled.'
    
    @app.get('/', response_class=HTMLResponse)
    async def main(request: Request):
        return templates.TemplateResponse('index.html', {'request': request})
    

    templates/index.html

    <!DOCTYPE html>
    <html>
       <head>
          <meta charset="utf-8">
          <meta name="viewport" content="width=device-width, initial-scale=1">
          <script>
             document.addEventListener('DOMContentLoaded', (event) => {
                document.getElementById("myForm").addEventListener("submit", function (e) {
                   var myForm = document.getElementById('myForm');
                   var catName = document.getElementById('catName').value;
                   myForm.action = '/disable/' + catName;
                });
             });
          </script>
       </head>
       <body>
          <h1>Disable a category</h1>
          <form method="post" id="myForm">
             <label for="catName">Enter a category name to disable:</label><br>
             <input type="text" id="catName" name="catName">
             <input class="submit" type="submit" value="Submit">
          </form>
       </body>
    </html>
    

    Option 4

    If you would like to prevent the page from reloading/redirecting when hitting the submit button of the HTML <form> and rather get the results in the same page, you could use Fetch API, as briefly described earlier, in order to make an asynchronous HTTP request, similar to this answer, as well as this answer and this answer. Additionally, one could call the Event.preventDefault() function, as described in this answer, in order to prevent the default action when submitting an HTML <form>.

    The example below is based on the previous option (i.e., Option 3); however, the same approach below (i.e., making an asynchronous HTTP request) could also be used for Options 1 & 2 demonstrated earlier, if you would like to keep the browser from refreshing the page on <form> submission. Note that this option submits the <input> value as a path parameter to the backend, but if you would like to submit it as a Form parameter instead, please have a look at the relevant code given in Option 1 earlier.

    app.py

    from fastapi import FastAPI, Request
    from fastapi.responses import HTMLResponse
    from fastapi.templating import Jinja2Templates
    
    app = FastAPI()
    templates = Jinja2Templates(directory='templates')
    
    @app.post('/disable/{name}')
    async def disable_cat(name: str):
        return f'{name} category has been disabled.'
    
    @app.get('/', response_class=HTMLResponse)
    async def main(request: Request):
        return templates.TemplateResponse('index.html', {'request': request})
    

    templates/index.html

    <!DOCTYPE html>
    <html>
       <head>
          <meta charset="utf-8">
          <meta name="viewport" content="width=device-width, initial-scale=1">
          <script>
             document.addEventListener('DOMContentLoaded', (event) => {
                document.getElementById("myForm").addEventListener("submit", function (e) {
                   e.preventDefault() // Cancel the default action
                   var catName = document.getElementById('catName').value;
                   fetch('/disable/' + catName, {
                         method: 'POST',
                      })
                      .then(resp => resp.text()) // or, resp.json(), etc.
                      .then(data => {
                         document.getElementById("response").innerHTML = data;
                      })
                      .catch(error => {
                         console.error(error);
                      });
                });
             });
          </script>
       </head>
       <body>
          <h1>Disable a category</h1>
          <form id="myForm">
             <label for="catName">Enter a category name to disable:</label><br>
             <input type="text" id="catName" name="catName">
             <input class="submit" type="submit" value="Submit">
          </form>
          <div id="response"></div>
       </body>
    </html>