Deleting Data in Applications
Learning Objectives
- You can implement a basic delete flow from a web application.
- You can explain why deleting data deserves extra care.
- You can identify a target row through a path parameter in a route.
Once the application can create rows, the next natural question is: how can users remove rows?
A delete flow involves:
- identifying an existing row,
- running a delete query against that row,
- and returning to a stable page.
Deleting One Chosen Row
The key question in a delete flow is: which exact row should be removed?
The answer is: the row that matches a given identifier. The identifier is a value that uniquely identifies one row in the table. In the tags table, the id column is a good identifier because it is unique for each tag.
In app/main.py, a delete route might look like this:
@app.post("/tags/{tag_id}/delete")
def delete_tag(tag_id: int):
with get_connection() as conn:
conn.execute(t"DELETE FROM tags WHERE id = {tag_id}")
return RedirectResponse("/tags", status_code=303)
The key idea is simple:
- the route receives one identifier from the URL path,
- the query deletes the matching row as a parameterized
DELETE, - and the application redirects back to the list page.
The value tag_id comes from the URL path. FastAPI reads it, converts it to an int, and passes it into the tag_id parameter before the route function runs. The t-string in the query then carries that value into the SQL as a parameter, following the safe pattern from the parameterization chapter.
The FastAPI documentation on path parameters is a useful reference for this mechanism.
In app/templates/tags.html, the list can include a delete button for each tag. This is a snippet to add inside the existing loop that renders the tags, because the form action needs each row’s identifier:
<ul>
{% for tag in tags %}
<li>
{{ tag.name }}
<form method="post" action="/tags/{{ tag.id }}/delete">
<button type="submit">Delete</button>
</form>
</li>
{% endfor %}
</ul>
The loop now renders one list item per tag, and each item contains the tag name together with a small form whose action embeds the current tag.id. When the user clicks the delete button, the browser sends a POST request to that specific delete URL with the tag’s identifier in the path.
For this to work, the
/tagslist route must also select theidcolumn, so thattag.idis available in the template. If the query only selectsname, the delete form will not work because the identifier is missing.
Why POST for Delete?
The delete route above uses POST, not GET.
That is useful because deleting data changes persistent state. A plain link is usually better reserved for navigation, while a submitted form makes it clearer that the action is intended to change data.
It also avoids accidental deletion. If delete were exposed as a GET URL, prefetching or a mistaken click on a stale tab could silently remove rows. A form submission is a more deliberate action.
What to Check After Deleting
Write operations deserve more attention than reads because mistakes change persistent state.
Useful habits here are:
- always identify the target row carefully,
- always parameterize user-provided values,
- think about whether the input should be validated,
- and think about where the user should end up after the action.
A useful mental model is to imagine the visible state before and after the action:
- before the delete, the tag appears in the list,
- after the delete, that row is gone.
If you check those before-and-after states deliberately, you make far fewer write-related mistakes.
The Tags Template After This Chapter
After adding the delete form inside the list, the whole 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 }}
<form method="post" action="/tags/{{ tag.id }}/delete">
<button type="submit">Delete</button>
</form>
</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 chapter adds an edit link next to the delete button, keeping both actions available in the same list item.
Check Your Understanding
- Why is the path parameter useful in a delete flow?
- Why is a delete button usually implemented as a
POSTform instead of a plain link? - Why is it worth checking the list page again after a delete?
Programming Exercise
This chapter’s programming exercise builds on the finished create flow from the previous chapter. It asks you to add a delete button and the matching delete route so one chosen tag can be removed safely.