Python FastAPI Tutorial (Part 5): Adding a Database - SQLAlchemy Models and Relationships
Based on Corey Schafer's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.
Persisting data requires replacing in-memory lists with SQLAlchemy ORM models backed by a real database file (blog.db).
Briefing
The core shift is replacing FastAPI’s in-memory “posts” list with a real SQLAlchemy-backed database so data persists across server restarts—and then wiring that database into the API, schemas, templates, and relationships. Using SQLite for development (no extra server needed), the setup creates a durable blog.db file and establishes proper user–post relationships, so posts can be tied to real user records instead of an author string. This matters because it turns a learning prototype into an architecture that can scale and later migrate to production databases like PostgreSQL with minimal code changes.
The implementation is built around three layers: SQLAlchemy database models, Pydantic schemas for the API contract, and FastAPI route handlers for request/response logic. Incoming requests get validated by Pydantic, SQLAlchemy stores or retrieves data via ORM models, and Pydantic formats the response back to JSON. That separation is emphasized as a practical design choice: database models handle ORM-specific features like foreign keys and relationships, while schemas define what the API accepts and returns.
A dedicated database configuration file defines the SQLAlchemy engine, session factory, and a FastAPI dependency that yields a per-request database session. For SQLite, the connection URL points to blog.db in the project directory, and the tutorial sets check_same_thread=False to accommodate FastAPI’s multi-threaded request handling. Sessions are configured with auto-commit and auto-flush disabled so commits happen explicitly. On startup, SQLAlchemy creates tables automatically using base.metadata.create_all(bind=engine), making the setup repeatable.
Two ORM models drive the data structure. The User model includes unique username and email fields, optional image_file storage, and a computed image_path property that selects between default static images and uploaded media under media/profile_pictures. A one-to-many relationship links users to posts: one user has many posts, and each post belongs to one author. The Post model includes title, content, a foreign key user_id referencing users.id (with an index for faster lookups), and a timezone-aware date_posted defaulted to the current UTC time. Time zone awareness is treated as a migration-friendly habit.
Pydantic schemas are updated to match the ORM models and relationships. User schemas validate username length and email format, and user response schemas enable reading ORM attributes (including the computed image_path property). Post schemas change from storing author as a string to returning a nested author object (user response) and include date_posted as a datetime that serializes to ISO 8601 automatically.
Routes are refactored to use dependency-injected database sessions and SQLAlchemy select queries. Creating a user checks for existing username and email before inserting, returning friendly HTTP 400 errors instead of relying only on database constraints. Fetching a user or posts uses 404 errors when records don’t exist, and the “get posts by user” endpoint distinguishes between “user not found” and “user exists but has no posts.” Templates are updated to reflect the new data shapes: post.author becomes an object (post.author.username, post.author.image_path), and date_posted is formatted for display in Jinja2.
Testing confirms the end-to-end behavior: blog.db is created, API endpoints return nested author data, error handling works for missing users/posts, and—crucially—data persists after restarting the server. The tutorial closes by noting that the next step is completing CRUD with update (PUT/PATCH) and delete operations.
Cornell Notes
FastAPI’s earlier in-memory posts list is replaced with SQLAlchemy ORM models backed by a persistent SQLite database (blog.db). The design uses three layers—SQLAlchemy models, Pydantic schemas, and FastAPI routes—so requests validate via Pydantic, data is stored/retrieved via SQLAlchemy, and responses are serialized back through Pydantic. User and Post models are linked with a one-to-many relationship: posts reference users via a foreign key (user_id), and API responses return nested author data automatically. The database session is injected per request using a FastAPI dependency, and tables are created on startup. Templates are updated to display author.username and format date_posted for readability, while the API keeps ISO 8601 timestamps.
Why does the tutorial insist on separating SQLAlchemy models from Pydantic schemas instead of using one combined model definition?
How does the database session get into each route, and why is that important?
What concrete changes happen when posts move from an in-memory list to ORM tables?
How are user–post relationships represented, and how does that affect API responses?
Why is date_posted stored as a timezone-aware datetime, and how is it displayed differently in the UI?
What’s the purpose of checking for existing username/email before inserting a new user?
Review Questions
- What components make up the tutorial’s three-layer architecture, and what job does each layer perform?
- How do foreign keys and relationships change what the API returns for a post compared with the earlier author-as-string approach?
- Where does the per-request SQLAlchemy session come from, and what does FastAPI do with it after the request finishes?
Key Points
- 1
Persisting data requires replacing in-memory lists with SQLAlchemy ORM models backed by a real database file (blog.db).
- 2
A per-request SQLAlchemy session is injected into routes via a FastAPI dependency (get_db), ensuring consistent transaction handling and cleanup.
- 3
User and Post are linked with a one-to-many relationship using a foreign key (Post.user_id → User.id), enabling nested author data in responses.
- 4
Pydantic schemas are updated to match ORM models, including from_attributes=True so computed properties like image_path and relationship objects serialize correctly.
- 5
Creating records now uses db.add(), db.commit(), and db.refresh(), while reads use SQLAlchemy select queries instead of list searches.
- 6
Routes return clear HTTP errors (400 for duplicate user fields, 404 for missing users/posts) to distinguish “not found” from “empty but valid.”
- 7
Templates must be updated to reflect new data shapes: post.author is an object (e.g., post.author.username) and date_posted is formatted for display.