Using Databases from Web Applications
Learning Objectives
- You can describe how a web request leads to a database query and then to a response.
- You can explain the roles of routes, connection helpers, and templates at a practical level.
A database is rarely used directly by end users. Instead, an application that uses the database through the database management system, such as a web application, sits between the user and the database. Most of the interaction happens through the application — through pages, forms, and links, if it’s a web application. In this chapter, we look at how one request becomes one database query and then one rendered response.
One Request, Many Small Steps
When a request arrives, it is handled by a route function. That route function may run one or more SQL queries against the database, and then it returns a response. The response is often created by rendering a template, which turns the query result into HTML.
At a high level, the request flow looks like this:
This is the basic pattern for many server-rendered database-backed pages.
HTTP Requests and FastAPI
The term “request” in web applications usually refers to an HTTP request, and terms such as GET or POST refer to the HTTP method of that request. HTTP is the standard web protocol that browsers and web applications use to send requests and responses.
HTTP supports several request methods. In this course, we use two of them:
GETis used to request a page or some content from the server.POSTis used to send submitted data to the server so that something can be created, updated, or otherwise processed.
So when the browser asks for /tags, that is usually a GET request. Later, when a form is submitted to create or edit a tag, that is usually a POST request.
It is useful to notice that the browser and the route do not work at exactly the same level. The browser sends and receives raw HTTP messages. FastAPI sits in between and translates those messages into Python-friendly values on the way in, and back into HTTP responses on the way out.
For example:
- a
GET /tagsrequest is matched to@app.get("/tags"), - submitted form fields can become
Form(...)parameters, - a path such as
/tags/3/editcan providetag_id=3, - and a
TemplateResponseorRedirectResponsebecomes an HTTP response that the browser understands.
You do not need to know the HTTP protocol in detail for this course. The important idea is that frameworks such as FastAPI remove a lot of repetitive parsing and formatting work, so routes can focus on the application logic.
An effective study habit in this part is to pick one visible page and inspect it in three places: the route, the query, and the template. The Project Roadmap page is useful when you are first learning where these pieces live, because it also includes a short file map and request-flow reminder.
The Shape of a Route
Each route has a shape, which is a common structure that appears again and again in different features. It has a line that defines the mapping, a block that runs one or more queries, and a return statement that creates a response.
For example, a route for listing tags might look like this (the SQL is not important here, only the shape):
@app.get("/tags")
def list_tags(request: Request):
with get_connection() as conn:
tags = conn.execute("...").fetchall()
return render(request, "tags.html", tags=tags)
The @app.get("/tags") line tells FastAPI that this function should handle GET requests to /tags. When such a request arrives, FastAPI calls this function and provides the Request object that the route needs.
The exact SQL matters, but the first idea to notice is the flow:
- the route receives the request,
- the route asks the database for rows,
- and the template turns those rows into HTML.
The final line uses two helpers that are already part of the project from the walking skeleton: get_connection() and render(). The rest of this chapter looks at what those helpers do and why they are kept small.
Dividing Responsibility
Each route usually has at least three responsibilities:
- receive the request,
- decide what data is needed,
- return a response.
The route is not, however, responsible for every detail of those steps. For example, the route does not need to know how to connect to the database or how to render HTML. Instead, it can call a helper function to get a database connection and call a template to render the response.
A helpful way to think about routes is that they coordinate the feature rather than doing every job themselves.
The responsibility for connecting to the database is hidden in get_connection(), defined in app/db.py. The HTML templates used when rendering the responses live in app/templates/. The small render() helper that wraps the common template-response pattern is already defined in app/main.py and is used from every route in this part.
Database connection helper
The database connection helper in app/db.py looks as follows:
import os
import psycopg
from psycopg.rows import dict_row
def get_connection():
return psycopg.connect(
os.environ["DATABASE_URL"],
row_factory=dict_row,
)
The helper hides the repetitive work of opening a PostgreSQL connection with the correct environment variable and row format. The psycopg library abstracts away details of the PostgreSQL protocol and provides a Python API for running SQL queries.
The row_factory=dict_row part tells psycopg to return rows with column names as keys. This way, templates can access values such as tag.name instead of relying on indexes.
The
psycopgdocumentation on row factories explains this in more detail.
HTML templates
The folder app/templates/ contains HTML templates, which define how the application renders responses. We use a template engine called Jinja. It allows the templates to include dynamic values such as {{ tag.name }} and control structures such as {% for tag in tags %}.
The values correspond to the context that the route passes into the template. For example, if the route calls render(request, "tags.html", tags=tags), then the template can use tags as a variable that contains the query result.
A file called tags.html in app/templates/ might look 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>
</body>
</html>
This is a full template file. It renders a list of tags by looping over the tags variable and showing the name of each tag in a list item. The template does not know how the tags variable was created or what SQL query produced it. It only focuses on presentation, not on database logic.
The FastAPI documentation on templates is also useful here because FastAPI uses Jinja templates through that helper layer.
The render() helper
The walking skeleton already defines a small helper in app/main.py:
def render(request: Request, template_name: str, **context):
return templates.TemplateResponse(
request=request,
name=template_name,
context=context,
)
render() wraps the TemplateResponse pattern so that routes can stay short. Instead of writing:
return templates.TemplateResponse(
request=request,
name="tags.html",
context={"tags": tags},
)
a route can simply write:
return render(request, "tags.html", tags=tags)
The two versions do the same thing. Using render() consistently keeps the routes easier to scan, and the rest of this part uses it everywhere.
With these helpers in place, the full GET /tags route can stay short:
@app.get("/tags")
def list_tags(request: Request):
with get_connection() as conn:
tags = conn.execute("...").fetchall()
return render(request, "tags.html", tags=tags)
The next chapter turns this same pattern into one complete read-only page.
Check Your Understanding
- In the route shape above, which line actually talks to PostgreSQL?
- Why is
get_connection()useful even though it does not hide SQL? - Why is it useful that the template receives ready-made rows instead of opening the database itself?
AI Study Aid
Create a chapter diagram
We're looking into to what extent AI-generated diagrams could help with studying.
Use this study aid to generate an AI-generated visual summary of the material you just studied.
Each diagram style emphasizes the content in a different way, so you can choose the focus
that feels most useful.
Using the diagram generator is voluntary and does not affect your course progress. We encourage
you to try it out and see if it helps, but it's totally up to you! Your answers help us understand
how to make better study aids in the future.
Selected style: choose a diagram style
Programming Exercise
This chapter’s programming exercise is the first real /tags page in the guided tags practice track. It starts from the completed walking skeleton from the previous chapter and asks you to add a route and template that list the tags from the database with the newest tag first, based on the creation time.