Forms and Submission
Learning Objectives
- You can connect a form’s method, action, and field names to a route.
- You can pass submitted text to a supplied application operation.
- You can explain why an addition redirects to a new GET request.
Search forms request a selection. An addition form requests a change. Continue the web-app project from Chapter 6. The local setup below supplies an operation that adds a record; we will connect form fields to it.
The new form asks for a title and category. The supplied operation generates the identifier.
A Form That Submits a Value
Begin with one field. The form’s method="post" asks the browser to submit field values in a POST request to its action:
<form method="post" action="/tasks/add"> <label for="new-title">Title</label> <input id="new-title" name="title" value=""> <button type="submit">Add task</button></form>The id connects the input to the label’s for value. The name determines the submitted field’s name. The value determines the initial displayed text: value="" displays an empty field. If the context supplies a task dictionary, value="{{ task['title'] }}" displays its title in the field.
Warm up with small changes to the one-field form in templates/page.html.
The browser uses method and action to choose the request and sends the named field values in its body. FastAPI matches the method and path, reads the fields declared with Form, and calls the registered function with those values:
| Incoming information | Declaration in the route | Value available inside the function |
|---|---|---|
POST to /tasks/add | @app.post("/tasks/add") | Selects this route. |
Field named title containing Prepare a summary | title: str = Form("") | The string "Prepare a summary". |
Field named category containing writing | category: str = Form("") | The string "writing". |
str describes the expected text value. Form("") uses "" if the field is absent. GET and POST can share a path because FastAPI matches both the method and the path.
The next two exercises use a one-title form. First, connect a form to a route that previews its submitted title.
A Task Form's Field Connections
0 / 15 points
Repair the label’s for and the input’s name in templates/page.html. Match for to the input’s id, so clicking the label focuses it. Match name to the receiving POST parameter in main.py, so the preview displays the submitted title.
Adding a Record and Redirecting
Inside the POST route, these two lines add a task and send the browser back to the list. The supplied add_task operation creates and saves the record:
add_task(DATA_FILE, title, category)return RedirectResponse("/tasks", status_code=303)After the addition, the response has status 303 and a destination. The browser then makes a new GET request to that destination. The addition happens during POST; the subsequent GET displays the current records. Refreshing the resulting page repeats the GET rather than the addition.
Follow the separate POST and GET requests in the diagram below:
POST performs the addition once; the redirect and later refresh display it through GET.
Apply the same request sequence to the one-title application. Its supplied add_task(title) appends a title to an in-memory list. The local application below saves complete records in JSON.
A Task Addition and Redirect
0 / 20 points
Complete the two marked statements in add_from_form in main.py. Call the supplied add_task once with the submitted title. Then return a 303 redirect to the GET route that displays the task list; find its destination in the supplied route registration.
A refresh repeats the request that produced the current page. Trace which route runs in the next question.
After Submitting a Form
0 / 5 points
Each POST to /tasks adds the submitted title to task_titles and redirects to GET /tasks. The GET route displays the titles using tasks.html:
task_titles = ["Read the article", "Practise loops"]
@app.get("/tasks")def show_tasks(request: Request): return templates.TemplateResponse( request=request, name="tasks.html", context={"task_titles": task_titles} )A visitor submits Prepare a presentation once. The browser follows the redirect, displays the page, and the visitor refreshes it.
Which request does the refresh make, and how many copies of the submitted title are in task_titles afterward?
Add the Form to the Local Project
Extend the existing files in web-app using the listings below. Keep the detail route, detail template, and selection function.
Supplied Addition and Storage Code
Append this block to support.py, after read_records. It uses the definitions supplied in Chapter 4. Chapter 9 examines how add_task coordinates these helpers.
from uuid import uuid4
def write_records(path, records): with open(path, "w", encoding="utf-8") as destination: json.dump(records, destination, ensure_ascii=False, indent=2)
def prepare_task(title, category): return { "id": uuid4().hex, "title": title.strip(), "category": category.strip(), }
def add_task(path, title, category): records = read_records(path) task = prepare_task(title, category) records.append(task) write_records(path, records) return taskuuid4().hex supplies a generated text identifier.
Connect the Form in Python and HTML
In main.py, replace the existing imports from fastapi, fastapi.responses, and support with these lines. Keep the Jinja2Templates import:
from fastapi import FastAPI, Form, Requestfrom fastapi.responses import PlainTextResponse, RedirectResponsefrom support import read_records, add_taskKeep the existing GET routes and add this POST route below them:
@app.post("/tasks/add")def add_from_form(title: str = Form(""), category: str = Form("")): add_task(DATA_FILE, title, category) return RedirectResponse("/tasks", status_code=303)Replace templates/tasks.html with this document. It adds the new form below the search results:
<!doctype html><html lang="en"> <head> <meta charset="utf-8"> <title>Study tasks</title> </head> <body> <h1>Study tasks</h1> <form id="task-search" method="get" action="/tasks"> <label for="search-title">Title contains</label> <input id="search-title" name="q" value="{{ q }}"> <button type="submit">Search</button> </form>
{% if tasks %} <ul id="task-list"> {% for task in tasks %} <li> <a href="/tasks/{{ task['id'] }}">{{ task["title"] }}</a> ({{ task["category"] }}) </li> {% endfor %} </ul> {% else %} <p>No tasks.</p> {% endif %}
<h2>Add a task</h2> <form id="new-task" method="post" action="/tasks/add"> <label for="new-title">Title</label> <input id="new-title" name="title" value=""> <label for="new-category">Category</label> <input id="new-category" name="category" value=""> <button type="submit">Add task</button> </form> </body></html>Follow the Form Submission
Add title Prepare a summary with category writing. The browser returns to /tasks, where the new record appears. Refresh: it still appears once, and the addition form has empty fields.
Use the print-debugging approach from Chapter 2 in main.py. Add this line at the start of add_from_form, before the call to the supplied operation:
print("received fields:", repr(title), repr(category))Submit title Review the next chapter with category reading. The server terminal should show:
received fields: 'Review the next chapter' 'reading'The print shows the submitted strings before the operation trims them. Remove it after following the addition.
Check Your Understanding
- A visitor submits the Search form on
/tasks. Why are the addition form’s title and category inputs not part of that request? - The server prints the submitted title correctly but prints
''for category, although the visitor filled in both fields. Which connection between the category input and the POST function would you inspect first? - The terminal print shows spaces around a title, but the saved title has none. What does this tell you about when the submitted text is prepared for storage?
- The addition operation runs, but the browser never makes the following GET request for
/tasks. Which information in the POST response would you check?