Project Structure & App Factory
The single-file app.py from the previous lesson is genuinely fine for a quick demo,
but the moment Bookshelf grows real models, real forms, and more than a couple of routes, that one
file starts working against you. This lesson covers why, and introduces the pattern Flask
projects reach for once that happens: the application factory.
Why a Single File Stops Scaling
A bare module-level app = Flask(__name__) works well right up until two specific
problems show up in a growing project:
- Circular imports — once
models.pyneeds to reference the app (for the database connection) and routes need to import frommodels.py, and the main file needs to import routes, Python's import system can tie itself in knots trying to load everything in the right order - No clean way to configure differently per environment — a single global
appobject is created exactly once, at import time, with whatever configuration was hardcoded. Testing Bookshelf against a separate test database, or running it with production settings, becomes awkward when there's only ever one shared app instance to configure
Neither problem is fatal for a small project, but both get worse, not better, as Bookshelf adds more models, more routes, and more configuration needs.
The Application Factory Pattern
Flask's answer is to stop creating the app at import time altogether. Instead of a bare
module-level app, you write a function — conventionally named
create_app() — that builds a Flask instance, configures it, and
returns it. Nothing calls that function until something actually needs a running app.
Example: A Minimal App Factory
from flask import Flask
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
return app
Every place that needs an app — the real development server, the test suite, a production
WSGI server — calls create_app() itself and gets back a freshly built instance,
configured however that caller needs.
Why This Pattern Helps Testing Specifically
This is where the factory pattern pays for itself the most clearly. With a single shared
app object, every test in a test suite runs against the exact same configuration,
because there's only one instance to configure. With create_app(), each test (or each
test session) can call the factory with test-specific configuration — a separate in-memory
test database, debugging turned off, a different secret key — and get back a completely
fresh app instance that doesn't affect any other test.
Book and
assigning it to a Shelf works correctly" can build its own throwaway app and database
for just that test, run it, and discard it — without ever touching the real development
database backing your actual Bookshelf data. The dedicated
Testing with Pytest lesson later in this track builds on
exactly this factory pattern.
A Realistic Bookshelf Project Layout
Once Bookshelf outgrows one file, a common way to organize it is as a Python package — a
folder containing an __init__.py — alongside a small top-level script that
starts the app:
bookshelf/
bookshelf/
__init__.py (contains create_app())
models.py (User, Shelf, Book, Tag)
routes.py (view functions)
templates/ (Jinja2 templates)
static/ (CSS, JS, images)
config.py
run.py
requirements.txt
The inner bookshelf/ folder is the actual Python package; everything that makes up
the application — the factory function, the models, the routes, the templates, the static
assets — lives inside it. config.py and run.py sit one level up,
outside the package, since they're about running and configuring the project rather than being
part of the application itself.
Building It: create_app() and run.py
Filling in the factory a bit further, bookshelf/__init__.py builds the app,
configures it, and hooks up the routes defined in routes.py:
Example: bookshelf/__init__.py
from flask import Flask
from config import Config
from .routes import register_routes
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
register_routes(app)
return app
Example: bookshelf/routes.py
def register_routes(app):
@app.route('/')
def home():
return 'Welcome to Bookshelf'
@app.route('/books')
def book_list():
return 'Your books will appear here'
This simple register_routes(app) function is a small stand-in for what
Blueprints handle more elegantly once Bookshelf has routes
spread across several files — that lesson picks this exact spot back up. For now, it's
enough to keep route registration out of the factory function itself.
Finally, run.py at the project root is the script you actually execute to start the
development server:
Example: run.py
from bookshelf import create_app
app = create_app()
if __name__ == '__main__':
app.run(debug=True)
With this in place, python run.py imports the bookshelf package, calls
create_app() to build a configured instance, and runs it — the same development
server experience as before, now backed by a structure that can keep growing.
Growing Into the Pattern, Not Starting With It
None of this means a small, single-file app.py is a mistake. It's a completely
reasonable way to start — the previous lesson's version is real, working Flask code. The
application factory pattern is something you grow into once a project's actual complexity earns
it: once models, routes, and configuration needs start pulling in different directions is exactly
the right time to restructure, not before. Introducing a package layout and a factory function on
day one, before there's anything to organize, usually just adds ceremony without benefit.
Config object to live, head to Configuration & Environments to see what actually goes inside it.