When you start building a Flask application, writing everything in a single file feels natural. But as your project grows, this approach becomes problematic. You end up with hundreds of lines of code in one file, circular imports, and a codebase that’s hard to maintain or test.
Flask Blueprints solve this problem by letting you organize your application into logical components. Think of them as mini-applications that you can plug together. Combine this with the Application Factory pattern, and you get a structure that scales from a simple blog to a production system handling thousands of users.
This tutorial walks you through building a Flask application using Blueprints. You’ll learn how to separate concerns, manage configuration, and avoid common mistakes that trip up developers.
Prerequisites
Before starting, make sure you have:
- Python 3.8 or higher installed
- Basic understanding of Flask (routing, templates, request handling)
- Familiarity with virtual environments
Install Flask in your project environment:
pip install flask flask-sqlalchemy python-dotenv
Step 1: Understanding the Application Factory Pattern
The Application Factory is a function that creates and configures your Flask app. Instead of creating a global app object, you generate it on demand. This approach offers several benefits:
- You can create multiple app instances with different configurations
- Testing becomes easier (each test can use a separate app instance)
- You avoid circular imports
Here’s a basic factory:
# app/__init__.py
from flask import Flask
def create_app(config_name='development'):
app = Flask(__name__)
# Load configuration
if config_name == 'production':
app.config.from_object('config.ProductionConfig')
else:
app.config.from_object('config.DevelopmentConfig')
return app
This function creates a new Flask instance each time it runs. You pass in a configuration name to control which settings get loaded.
To use it:
# run.py
from app import create_app
app = create_app('development')
if __name__ == '__main__':
app.run()
Step 2: Creating Your First Blueprint
A Blueprint defines a collection of routes, templates, and static files. You register it with your app, and it becomes part of your application.
Create a Blueprint for a blog:
# app/blog/routes.py
from flask import Blueprint, render_template
blog_bp = Blueprint('blog', __name__, url_prefix='/blog')
@blog_bp.route('/')
def index():
return render_template('blog/index.html')
@blog_bp.route('/post/<int:post_id>')
def post(post_id):
return render_template('blog/post.html', post_id=post_id)
The url_prefix parameter adds /blog before all routes in this Blueprint. So the index route becomes /blog/, and the post route becomes /blog/post/<int:post_id>.
Register the Blueprint in your factory:
# app/__init__.py
from flask import Flask
from app.blog.routes import blog_bp
def create_app(config_name='development'):
app = Flask(__name__)
if config_name == 'production':
app.config.from_object('config.ProductionConfig')
else:
app.config.from_object('config.DevelopmentConfig')
# Register blueprints
app.register_blueprint(blog_bp)
return app
Your project structure now looks like this:
project/
├── app/
│ ├── __init__.py
│ └── blog/
│ ├── __init__.py
│ └── routes.py
├── config.py
└── run.py
Step 3: Organizing Models and Database Access
Database models should live in their own module. This keeps them separate from route logic and makes them reusable across Blueprints.
Set up SQLAlchemy in your factory:
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def create_app(config_name='development'):
app = Flask(__name__)
if config_name == 'production':
app.config.from_object('config.ProductionConfig')
else:
app.config.from_object('config.DevelopmentConfig')
# Initialize extensions
db.init_app(app)
# Register blueprints
from app.blog.routes import blog_bp
app.register_blueprint(blog_bp)
return app
Create your models:
# app/models.py
from app import db
from datetime import datetime
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
author_id = db.Column(db.Integer, db.ForeignKey('user.id'))
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
posts = db.relationship('Post', backref='author', lazy=True)
Use these models in your Blueprint:
# app/blog/routes.py
from flask import Blueprint, render_template
from app.models import Post
blog_bp = Blueprint('blog', __name__, url_prefix='/blog')
@blog_bp.route('/')
def index():
posts = Post.query.order_by(Post.created_at.desc()).all()
return render_template('blog/index.html', posts=posts)
@blog_bp.route('/post/<int:post_id>')
def post(post_id):
post = Post.query.get_or_404(post_id)
return render_template('blog/post.html', post=post)
Step 4: Managing Configuration for Different Environments
Hardcoding configuration values makes your app inflexible. You need different settings for development, testing, and production.
Create a configuration module:
# config.py
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///dev.db'
class ProductionConfig(Config):
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///test.db'
Store sensitive values in environment variables:
# .env
SECRET_KEY=your-secret-key-here
DATABASE_URL=postgresql://user:pass@localhost/dbname
Load them with python-dotenv:
# run.py
import os
from dotenv import load_dotenv
from app import create_app
load_dotenv()
env = os.environ.get('FLASK_ENV', 'development')
app = create_app(env)
if __name__ == '__main__':
app.run()
Step 5: Adding Authentication as a Separate Blueprint
Authentication logic deserves its own Blueprint. This keeps user management separate from your other features.
Create the auth Blueprint:
# app/auth/routes.py
from flask import Blueprint, render_template, redirect, url_for, flash, request
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import login_user, logout_user, login_required
from app.models import User
from app import db
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
if User.query.filter_by(email=email).first():
flash('Email already registered')
return redirect(url_for('auth.register'))
user = User(
username=username,
email=email,
password=generate_password_hash(password)
)
db.session.add(user)
db.session.commit()
return redirect(url_for('auth.login'))
return render_template('auth/register.html')
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
email = request.form.get('email')
password = request.form.get('password')
user = User.query.filter_by(email=email).first()
if user and check_password_hash(user.password, password):
login_user(user)
return redirect(url_for('blog.index'))
flash('Invalid credentials')
return render_template('auth/login.html')
@auth_bp.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('blog.index'))
Set up Flask-Login in your factory:
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
db = SQLAlchemy()
login_manager = LoginManager()
def create_app(config_name='development'):
app = Flask(__name__)
if config_name == 'production':
app.config.from_object('config.ProductionConfig')
else:
app.config.from_object('config.DevelopmentConfig')
db.init_app(app)
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
# Register blueprints
from app.blog.routes import blog_bp
from app.auth.routes import auth_bp
app.register_blueprint(blog_bp)
app.register_blueprint(auth_bp)
return app
@login_manager.user_loader
def load_user(user_id):
from app.models import User
return User.query.get(int(user_id))
Your structure now looks like this:
project/
├── app/
│ ├── __init__.py
│ ├── models.py
│ ├── blog/
│ │ ├── __init__.py
│ │ └── routes.py
│ └── auth/
│ ├── __init__.py
│ └── routes.py
├── templates/
│ ├── blog/
│ │ ├── index.html
│ │ └── post.html
│ └── auth/
│ ├── login.html
│ └── register.html
├── config.py
├── .env
└── run.py
Common Pitfalls
Circular Imports: This happens when two modules import each other. The solution is to move imports inside functions or use lazy loading. For example, import Blueprints inside your factory function instead of at the module level.
# Bad
from app.blog.routes import blog_bp
def create_app():
app = Flask(__name__)
app.register_blueprint(blog_bp)
return app
# Good
def create_app():
app = Flask(__name__)
from app.blog.routes import blog_bp
app.register_blueprint(blog_bp)
return app
Global Database Objects: Don’t create your database connection outside the factory. Use db.init_app(app) instead of passing the app directly to SQLAlchemy(app). This lets you create multiple app instances with different database connections.
Blueprint Name Collisions: Each Blueprint needs a unique name. The first argument to Blueprint() becomes the namespace for url_for(). If two Blueprints share the same name, Flask will only register the first one.
Static File Confusion: Blueprints can have their own static folders, but this can get confusing. Keep all static files in the main app’s static folder unless you have a good reason to separate them.
Summary
Flask Blueprints turn your monolithic application into a collection of logical components. The Application Factory pattern adds flexibility by letting you create app instances on demand with different configurations.
Here’s what you learned:
- The factory pattern creates app instances programmatically
- Blueprints organize routes into logical groups
- Models live in a shared module that all Blueprints can import
- Configuration classes handle different environment settings
- Each feature (blog, auth) gets its own Blueprint
This structure scales well. When you need to add a new feature, create a new Blueprint. When you need to change configuration, modify the config classes. When you need to test something, create a test app instance with test settings.
Start small with one or two Blueprints. As your application grows, you’ll see where natural divisions exist in your code. That’s when you create new Blueprints to keep things organized.
Discussion
Leave a comment
No comments yet
Be the first to start the conversation.