Best Practices

This is the last stop on a 37-lesson track, and Bookshelf has by now touched nearly every corner of Flask — routing, templates, forms, a real database layer, real user authentication, a REST API, testing, caching, background tasks, and deployment. Rather than introduce anything new, this final lesson pulls together ten habits worth carrying forward into any Flask project you build from here — a checklist, not a wall of new theory.

1. Use the App Factory Pattern, Even for a Small Project

Project Structure & App Factory covered create_app() back near the very start of this track, before there was much reason to need it yet. It costs almost nothing upfront, and it pays off the exact moment a project needs more than one configuration — testing, as in Testing with Pytest, or separate dev/production settings.

def create_app(config_name='development'):
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    return app
  • Write create_app() on day one, even for a single-file prototype.
  • A project without it usually needs a painful retrofit the first time it needs a second config.

2. Keep Views Thin

A Flask view's job is to receive a request and produce a response — not to host multi-step business logic. When a view starts accumulating logic about how a book's status changes or how a reading streak is calculated, that logic belongs on the model itself, or in a dedicated service function the view simply calls.

# Model method, not view logic
class Book(db.Model):
    # ...columns...
    def mark_finished(self, rating):
        self.status = 'finished'
        self.rating = rating
        db.session.commit()
  • Views orchestrate; models and service functions decide.
  • If a view is hard to summarize in a sentence, logic has probably crept in that doesn't belong there.

3. Reach for db.relationship() and backref Deliberately

Models & Relationships covered how db.relationship() gives Bookshelf's Shelf a readable shelf.books collection, instead of a manual query every time a shelf's books are needed. Naming these deliberately, every time, keeps reverse queries readable throughout the codebase.

class Shelf(db.Model):
    # ...columns...
    books = db.relationship('Book', backref='shelf', lazy=True)
  • Set a relationship deliberately on every meaningful foreign key, not just where it happens to be convenient.
  • Pick a name that reads naturally: shelf.books, book.shelf.

4. Never Store Secrets in Code

Configuration & Environments and Security Best Practices both made the same point: every secret — SECRET_KEY, a database password, a mail server credential, a JWT signing key — comes from an environment variable, never a literal string committed to source control.

app.config['SECRET_KEY'] = os.environ['SECRET_KEY']  # not a hardcoded string
  • Every secret comes from the environment, never a literal in a committed file.
  • .env stays in .gitignore; only .env.example gets committed.

5. Use Flask-WTF and Marshmallow Instead of Hand-Rolling Validation

Flask gives you no validation for free — Forms with Flask-WTF and Marshmallow Serialization & JWT Auth both exist to fill exactly that gap declaratively, instead of writing manual if checks against request.form or request.get_json() by hand throughout the codebase.

class BookSchema(ma.SQLAlchemyAutoSchema):
    class Meta:
        model = Book
        load_instance = True
  • Model-derived validation (Flask-WTF fields, a Marshmallow schema) beats a hand-rolled check every time it's available.
  • Reach for a fully manual check only when there's genuinely no model or form behind the data being handled.

6. Check Object-Level Ownership Explicitly

Authorization & Custom Decorators is worth re-reading with this in mind: Flask has no built-in permission system to lean on the way Django does. A custom decorator answers "is this user logged in?" — it says nothing on its own about "does this specific Book belong to this specific user?" That check has to be written explicitly, every time it matters.

def edit_book(book_id):
    book = Book.query.get_or_404(book_id)
    if book.user_id != current_user.id:
        abort(403)
    # ...proceed with the edit...
  • A user being logged in doesn't mean they should be allowed to edit every row in the database.
  • Ownership checks belong in the view (or a shared decorator) — Flask won't add this for you.

7. Organize a Growing App into Blueprints

Blueprints covered splitting Bookshelf's single, growing app.py into focused modules — a books blueprint, an auth blueprint, an API blueprint. Doing this before the file becomes unmanageable is far less painful than doing it after.

app.register_blueprint(books_bp, url_prefix='/books')
app.register_blueprint(auth_bp, url_prefix='/auth')
  • Split into blueprints by feature area, not by file size alone.
  • A single-file Flask app is fine at the start — the goal is noticing the right moment to stop being one.

8. Write Tests for Your Critical Paths

Testing with Pytest made the same point worth repeating here: 100% coverage is a vanity metric. Testing the paths where a bug would genuinely hurt — authentication, anything touching data integrity — is worth far more than padding out a coverage percentage on templates that only render static text.

def test_anonymous_user_redirected_from_my_shelves(client):
    response = client.get('/shelves/mine')
    assert response.status_code == 302
  • Prioritize tests around login, ownership checks, and anything that writes or deletes data.
  • A smaller suite covering the critical paths well beats a large suite covering everything shallowly.

9. Reach for Celery Only When the Complexity Is Earned

Background Tasks with Celery was upfront about this: Celery solves a real problem, but it adds a broker and worker processes that now need to be run and monitored. Reach for it once something is genuinely too slow for a request/response cycle — not as a default architectural choice on day one.

# Worth the complexity: a slow, unavoidable operation
send_welcome_email_task.delay(user.id)
  • Ask whether the operation is actually too slow for a normal request before reaching for a background task.
  • Every new moving piece (broker, worker) is something that can fail — add it because you need it, not by default.

10. Pick the Right Small Pieces, Rather Than Fighting a Big Framework

Every lesson in this track — Flask-SQLAlchemy, Flask-WTF, Flask-Login, Flask-Marshmallow, Celery — was a deliberate, separate choice, not a bundled default. That's Flask's whole philosophy: compose exactly the small pieces a project actually needs, and skip the ones it doesn't. Resist the urge to reach for a heavy extension out of habit when a simple one — or plain Python — solves the problem just as well.

  • Before adding a new extension, ask whether the problem is actually big enough to need it.
  • A small, well-understood stack of pieces beats a large one nobody on the project fully understands.
Note: Every example above reflects correct, real Flask usage — but as with every lesson in this track, this website's own project doesn't run a live Flask server, so none of it executes on this page.

Congratulations on finishing all 37 lessons. You didn't just read about Flask in the abstract — through Bookshelf, one connected example carried from the very first route all the way here, you've built a genuine mental model of how a real Flask application fits together: routing and Jinja2 templates rendering pages, a real database layer with SQLAlchemy modeling users, shelves, books, and tags, forms and file uploads collecting real input safely, real user authentication and authorization deciding who can do what, a REST API with Marshmallow and JWT exposing that same data beyond the browser, and the testing, caching, background-task, and deployment knowledge to actually ship it and keep it running. That's the complete picture, start to finish, for a framework whose entire philosophy is giving you exactly the pieces you need and no more. From here, the "Next" button below leads back to the Python hub — a good place to branch out into Core Python, Django, or Data Science, whenever you're ready for what comes next.