Use a database in FastAPI
Create a request-scoped database dependency, use parameterized queries, and keep transactions explicit in FastAPI routes.
2 min readUpdated 3 exercises
Key takeaways
- Give each request its own database session or connection through a dependency and close it reliably.
- Pass user values as query parameters rather than building SQL with string interpolation.
- Commit successful writes, roll back failures, and translate expected database errors into HTTP responses.
Make the connection a dependency
An in-memory list disappears when the process restarts. For durable data, open a database resource for the request and release it afterwards. This small SQLite example uses a synchronous driver, so its route is an ordinary def:
import sqlite3
from collections.abc import Iterator
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
def get_db() -> Iterator[sqlite3.Connection]:
db = sqlite3.connect("books.db")
db.row_factory = sqlite3.Row
try:
yield db
finally:
db.close()
@app.get("/books/{book_id}")
def read_book(book_id: int, db: Annotated[sqlite3.Connection, Depends(get_db)]):
row = db.execute("SELECT id, title FROM books WHERE id = ?", (book_id,)).fetchone()
if row is None:
raise HTTPException(status_code=404, detail="Book not found")
return dict(row)This assumes a books table already exists. Create schema through a migration or application setup, not during every request. A production database often uses an ORM session instead of a raw connection, but the request ownership rule is the same.
Bind values; decide when to commit
The ? placeholder tells SQLite to bind book_id as data. Never assemble user input into SQL text with an f-string. For a write, call commit() only after all related statements succeed; call rollback() if an error occurs. If two changes must happen together, keep them in one transaction so a failure cannot leave half the work saved.
Map expected conditions to useful HTTP responses: a missing row to 404, a known uniqueness conflict to 409. Let unexpected database failures reach monitoring instead of presenting every error as a normal client mistake.
Match the driver to the route
FastAPI runs a plain def route in a worker thread in a normal server, so it can call a blocking database driver. An async def route should await an async database client; calling a blocking driver directly inside it stalls the event loop. For SQLite, also consider transaction contention under concurrent writes and use a server database when the workload outgrows it.
Now practice it
3 exercisesReading this page is the cheap half. These are the exercises that make you use it.
- Databases & Persistence · Models & CRUD BasicsReview connection ownership and safe query parameters.QuizTake · EasyQuizTake · Easy
- Databases & Persistence · Queries, Updates & TransactionsApply transaction and error mapping rules to write routes.QuizTake · MediumQuizTake · Medium
- Databases & Persistence · Session & Production BehaviorReason about concurrency, sessions, and database failures.QuizTake · HardQuizTake · Hard
Read next
Share logic with FastAPI dependencies
Use Depends to reuse request parsing and resource setup, compose dependencies, and replace them in tests.
1 min
Test FastAPI through HTTP
Exercise real request parsing and responses with TestClient, replace dependencies, and handle lifespan in async tests.
1 min
Transactions and isolation levels
What ACID actually promises, which anomalies each isolation level still allows, and why the read-then-write you wrote is a race unless you said otherwise.
7 min