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.py needs to reference the app (for the database connection) and routes need to import from models.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 app object 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.

Concretely for Bookshelf: a test that checks "creating a 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.

Next up: Now that Bookshelf has a place for a Config object to live, head to Configuration & Environments to see what actually goes inside it.