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.
.envstays in.gitignore; only.env.examplegets 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.
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.