A basic Flask application for understanding foundational Flask concepts.This README (and the docs it links to) is meant to explain not just what to run, but why the project is structured the way it is. Working on this application should provide you with an understanding of Flask concepts such as:
- Flask fundamentals: Request/response cycle, blueprints, and Flask's extension system.
- SQLAlchemy fundamentals: Data modeling, session lifecycle and transaction management, query construction with the ORM.
- Data migrations (Alembic): Updating databases, and record values when changes are made to the database schema via changes to the data models.
- Jinja2 template engine: Variable substitution, control structures, template inheritances, autoescaping, and more.
- Flask — web framework
- SQLAlchemy (via Flask-SQLAlchemy) — ORM / database access
- Flask-Migrate (Alembic) — database schema migrations
- Flask-WTF — HTML form handling/CSRF
- pytest — test runner
- uv — dependency management + running commands (this repo does not use plain
pip/venvdirectly) - SQLite — the database, for now (see
docs/database.md)
If you don't know one of these tools yet, that's the point of this project — look it up as you go.
Prerequisites: uv which is already installed on your CA laptop. That's it — uv manages the Python
version and virtual environment for you.
# Install dependencies (creates .venv/ automatically)
uv sync
# Apply database migrations (creates instance/app.db with all tables)
uv run flask db upgrade
# Run the dev server
uv run flask run
# -> visit http://127.0.0.1:5000/
# Run the tests
uv run pytestEvery command in this repo is prefixed with uv run instead of activating a virtualenv manually — uv run finds/uses
the project's .venv automatically. See uv's docs if this is new
to you.
uv run flask routes # list every registered route
uv run flask shell # open a Python shell with app context loaded
uv run flask db migrate -m "message" # generate a migration after changing a model
uv run flask db upgrade # apply pending migrations
uv run pytest -v # verbose test output
uv run pytest --cov=app --cov-report=term-missing # test coverage report
uv add <package> # add a new dependency (updates pyproject.toml + uv.lock).
├── app/
│ ├── __init__.py # create_app() — the application factory
│ ├── app.py # thin entrypoint: app = create_app()
│ ├── config.py # Config classes (Development/Testing/Production)
│ ├── extensions.py # Shared, unbound extension instances (db, ma, migrate)
│ ├── main/ # example blueprint (routes grouped by feature)
│ │ ├── __init__.py
│ │ └── routes.py
│ ├── models/ # SQLAlchemy models — one file per model (page.py is a real example)
│ ├── static/
│ └── templates/
├── tests/
│ ├── conftest.py # shared pytest fixtures (app, client, db)
│ ├── test_app_factory.py
│ ├── test_db.py
│ ├── test_page_model.py
│ └── test_routes.py
├── migrations/ # Alembic migration history (via Flask-Migrate) — committed to git
│ └── versions/
├── instance/ # gitignored; holds the local instance/app.db sqlite file
├── .env # gitignored; secrets/local overrides (currently empty)
├── .flaskenv # committed; tells the `flask` CLI how to find/run the app
├── pyproject.toml
└── docs/ # deep-dive guides — see below
This README covers the basics. For anything more involved, see the guides
in docs/:
| Guide | What's in it |
|---|---|
docs/architecture.md |
Why the app factory + blueprint pattern is used, and how the pieces fit together |
docs/configuration.md |
.env vs .flaskenv vs config.py — what goes where and why |
docs/adding-routes.md |
How to add a route to an existing blueprint, or create a new one |
docs/templates.md |
Base layout, nav/footer partials, and how the static-page template works |
docs/adding-models.md |
How to add a SQLAlchemy model |
docs/forms.md |
WTForms/Flask-WTF: what they are, CSRF protection, and building a form-backed route |
docs/database.md |
Where the DB lives per environment, and the Flask-Migrate/Alembic workflow for schema changes |
docs/testing.md |
How to write tests, the fixtures available, and a real gotcha about shared SQLAlchemy metadata across tests |
docs/improvements.md |
Running list of known gaps, caveats, and good next-step tasks across the project |
If you only read one doc, read docs/architecture.md first — it explains the app factory pattern that everything
else assumes you already understand.
Rough workflow for adding a feature end-to-end:
- Add a model —
docs/adding-models.md - Add a route (in an existing or new blueprint) —
docs/adding-routes.md - Add tests for both —
docs/testing.md - Run
uv run pytestbefore considering it done
Before committing your work, run uv run pre-commit install once (this also installs the commit-msg hook — see below).
After that, the hooks in .pre-commit-config.yaml run automatically on every commit:
ruff (lint + format),
mypy (type checking), bandit
(security linting), pip-audit (dependency vulnerability scanning), and some
general file format checkers. pre-commit and commitizen are both installed automatically via uv sync
(they're in the dev dependency group) — no separate global install needed.
This repo enforces Conventional Commits formatted commit messages, via
Commitizen running as a commit-msg hook. Once you've run
pre-commit install, any commit whose message doesn't match the required format is rejected:
<type>(<optional scope>): <description>
Allowed <type> values: feat, fix, build, bump, chore, ci, docs, perf, refactor, revert, style,
test.
git commit -m "feat: add page model and dynamic page route"
git commit -m "fix(routes): return 404 for missing page slug"
git commit -m "not following the format" # rejected by the commit-msg hookIf you'd rather not memorize the format, use Commitizen's interactive prompt instead of git commit directly — it asks
you for the type, scope, and description and builds a compliant message for you:
uv run cz commitWhy this matters beyond style: Conventional Commits is what makes automated changelog generation and semantic
version bumping possible later (cz bump, cz changelog) — the commit history itself becomes structured data instead
of free text.
This is a real, working app, not a polished one. Things intentionally left unfinished as learning opportunities / next steps:
SECRET_KEYdefaults to"dev"if.envdoesn't set one — fine for local development, not fine for anything production.- Only one blueprint (
main) and one real model (Post) — a good next exercise is adding a second model/blueprint pair following the same pattern (seedocs/adding-models.mdanddocs/adding-routes.md). Page.bodyis rendered with Jinja's|safefilter (raw HTML, no escaping) — fine for trusted/admin-authored content, a real risk if this ever accepts page content from untrusted users without sanitization. See the note inapp/templates/pages/post.html.- No admin UI for creating
Postrows — pages are currently created viaflask shellor a script; a form-based create/edit route (Flask-WTF is already a dependency for this) is a natural next feature.