Creating Data from Forms
Learning Objectives
- You can accept form data in a route and turn it into a safe insert.
- You can explain the basic request flow for creating new database rows from the browser.
A database-backed application becomes much more tangible when users can create new rows through the browser.
The next natural question is: how does the browser send new data to the application? The answer is: with a form and a POST request.
Creating One New Row
The question in a create flow is: which new row should be added?
The answer comes from the form input. In app/templates/tags.html, a small form can be placed after the tag list:
<h2>Create a Tag</h2>
<form method="post" action="/tags">
<label for="name">Tag Name</label>
<input id="name" name="name" type="text" />
<button type="submit">Create</button>
</form>
This is only the part to add to the existing tags.html template, not a full HTML file. When the user submits this form, the browser sends a POST request to /tags with the form data in the request body.
The Matching POST Route
In app/main.py, the matching route looks like this:
# add these to imports
from fastapi import Form
from fastapi.responses import RedirectResponse
# add this route
@app.post("/tags")
def create_tag(name: str = Form(...)):
with get_connection() as conn:
conn.execute(t"""
INSERT INTO tags (name)
VALUES ({name})
""")
return RedirectResponse("/tags", status_code=303)
The key idea is simple:
- the route receives the submitted value,
- the query inserts one new row,
- and the application redirects back to the list page.
The insert uses a parameterized t-string, as introduced in the previous chapter. The SQL structure lives in the template string, and name is passed separately as a data value. That keeps the query safe even though the value comes directly from the browser.
The form and the route also match each other in a very direct way:
method="post"matches@app.post("/tags"),action="/tags"matches the route path,name="name"matchesname: str = Form(...),- and the form does not need extra fields if the table only needs the tag name.
That is why the submitted values arrive in the route under those parameter names. The FastAPI documentation on Form Data describes this mechanism in more detail.
The Form(...) notation means that the value is required. FastAPI reads the submitted form data from the request body and passes the values into these parameters before the route function runs.
Why Redirect After Creating?
After the insert query, the route returns a RedirectResponse that sends the browser back to /tags.
The FastAPI documentation on response classes is useful if you want to see how
RedirectResponseis defined.
That redirect is useful for two reasons:
- the user sees the updated list page after the create action,
- and refreshing the page is less likely to repeat the form submission.
The 303 status code tells the browser to follow the redirect with a normal GET request to /tags.
This is called the Post/Redirect/Get pattern. It is a common way to handle form submissions in web applications.
With the redirect, the full form submission flow looks like this:
What to Check After Creating
Create flows are easier to debug when the visible steps are clear:
- the browser displays the form,
- the user submits the form,
- the route receives the form values,
- the insert stores a new row,
- the redirect returns the browser to
/tags.
It also helps to think about the before and after state:
- before: the tag row does not exist,
- after: the database contains the new tag and the list page includes it.
As the application grows, it is also worth asking what should count as valid input. For example:
- should the name be allowed to be empty?
- should duplicate tag names be allowed?
These are design questions rather than SQL questions. Later parts of the course look at them more carefully through constraints and validation.
The Tags Template After This Chapter
Because tags.html is extended across several chapters, it helps to see the whole file at the end of this one. After adding the create form, the full app/templates/tags.html looks like this:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Study Tracker</title>
</head>
<body>
<h1>Study Tracker</h1>
<h2>Tags</h2>
<ul>
{% for tag in tags %}
<li>{{ tag.name }}</li>
{% endfor %}
</ul>
<h2>Create a Tag</h2>
<form method="post" action="/tags">
<label for="name">Tag Name</label>
<input id="name" name="name" type="text" />
<button type="submit">Create</button>
</form>
</body>
</html>
The next two chapters add a delete button and an edit link inside the {% for tag in tags %} loop, so the list part of this page continues to grow.
Check Your Understanding
- Why is a
303redirect useful after a successful create action? - Why is
Form(...)useful in the route signature? - Why does the insert query use the
t-string form even though the input came from an HTML form?
Programming Exercise
This chapter’s programming exercise builds on the previous chapter’s finished read features. It asks you to add the create form and the matching POST /tags route, and then verify that the browser returns to the updated tag list.