Flask patterns -- application factory, blueprints, error handlers, extensions, request lifecycle, configuration, logging, CLI commands
98
98%
Does it follow best practices?
Impact
98%
1.28xAverage score across 5 eval scenarios
Passed
No known issues
Application factory, blueprints, error handling, extensions, request lifecycle, configuration, logging, CLI commands, and project structure for Flask.
project/
├── app/
│ ├── __init__.py # create_app() factory
│ ├── extensions.py # Extension instances (no app binding)
│ ├── errors.py # Custom exceptions + register_error_handlers()
│ ├── db.py # Database connection + queries
│ ├── models.py # Data classes / schemas
│ └── routes/
│ ├── __init__.py # Empty or blueprint imports
│ ├── menu.py # menu_bp
│ └── orders.py # orders_bp
├── tests/
│ ├── conftest.py # App + client fixtures using create_app(testing=True)
│ ├── test_orders.py
│ └── test_menu.py
├── requirements.txt
├── config.py # Config classes (Development, Testing, Production)
└── run.py # from app import create_app; app = create_app()Each route file defines exactly one Blueprint. Extensions live in extensions.py and are initialized in the factory. Error handlers are registered centrally, not per-blueprint.
Never create the app at module level. Use a create_app() factory function in app/__init__.py:
# WRONG: Module-level app makes testing impossible and causes circular imports
from flask import Flask
app = Flask(__name__)
@app.route('/items')
def get_items():
return jsonify([])# app/__init__.py
import os
from flask import Flask
def create_app(config_name=None):
app = Flask(__name__)
# Configuration
if config_name == 'testing':
app.config.from_mapping(
TESTING=True,
DATABASE_PATH=':memory:',
SECRET_KEY='test-secret',
)
else:
app.config.from_mapping(
DATABASE_PATH=os.getenv('DATABASE_PATH', 'data.db'),
SECRET_KEY=os.getenv('SECRET_KEY', 'dev-only-change-in-production'),
)
# Extensions -- use init_app pattern
from app.extensions import cors, limiter
cors.init_app(app)
limiter.init_app(app)
# Blueprints
from app.routes.menu import menu_bp
from app.routes.orders import orders_bp
app.register_blueprint(menu_bp, url_prefix='/api')
app.register_blueprint(orders_bp, url_prefix='/api')
# Error handlers
from app.errors import register_error_handlers
register_error_handlers(app)
# Request lifecycle hooks
from app.db import get_db
@app.teardown_appcontext
def teardown_db(exception):
db = getattr(app, '_database', None)
if db is not None:
db.close()
return appKey rules for create_app:
create_app and live in app/__init__.pyapp.run() inside create_app() -- the caller decides how to runCreate extension instances WITHOUT the app, then bind them in the factory:
# WRONG: Creates a global app dependency, breaks testing
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
cors = CORS(app)# app/extensions.py
from flask_cors import CORS
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
cors = CORS()
limiter = Limiter(key_func=get_remote_address)Then in the factory:
# Inside create_app()
from app.extensions import cors, limiter
cors.init_app(app)
limiter.init_app(app)Key rules:
extensions.py with NO app argumentinit_app(app) is called inside create_app() onlyGroup related routes into blueprints. Each blueprint lives in its own file under app/routes/.
# app/routes/orders.py
from flask import Blueprint, request, jsonify
from app.db import get_db
from app.errors import ValidationError, NotFoundError
orders_bp = Blueprint('orders', __name__)
@orders_bp.route('/orders', methods=['POST'])
def create_order():
data = request.get_json()
if not data or not data.get('customer_name', '').strip():
raise ValidationError('customer_name is required')
if not data.get('items'):
raise ValidationError('At least one item is required')
db = get_db()
order = db.create_order(data)
return jsonify({'data': order}), 201
@orders_bp.route('/orders/<int:order_id>')
def get_order(order_id):
db = get_db()
order = db.get_order(order_id)
if not order:
raise NotFoundError('Order', order_id)
return jsonify({'data': order})
@orders_bp.route('/orders')
def list_orders():
db = get_db()
orders = db.list_orders()
return jsonify({'data': orders})Key rules:
_bp (e.g., orders_bp, menu_bp)create_app() with app.register_blueprint(bp, url_prefix='/api')@app.route() directly -- always use blueprint routesapp.errors to raise custom exceptions (not return error dicts manually)# WRONG: Each route formats errors differently
@orders_bp.route('/orders/<int:order_id>')
def get_order(order_id):
order = db.get_order(order_id)
if not order:
return jsonify({'msg': 'not found'}), 404 # inconsistent shape# app/errors.py
from flask import jsonify
class AppError(Exception):
def __init__(self, message, status_code, code='ERROR'):
self.message = message
self.status_code = status_code
self.code = code
class NotFoundError(AppError):
def __init__(self, resource, id=None):
msg = f'{resource} {id} not found' if id else f'{resource} not found'
super().__init__(msg, 404, 'NOT_FOUND')
class ValidationError(AppError):
def __init__(self, message):
super().__init__(message, 400, 'VALIDATION_ERROR')
def register_error_handlers(app):
@app.errorhandler(AppError)
def handle_app_error(e):
return jsonify({'error': {'code': e.code, 'message': e.message}}), e.status_code
@app.errorhandler(404)
def handle_404(e):
return jsonify({'error': {'code': 'NOT_FOUND', 'message': 'Endpoint not found'}}), 404
@app.errorhandler(405)
def handle_405(e):
return jsonify({'error': {'code': 'METHOD_NOT_ALLOWED', 'message': 'Method not allowed'}}), 405
@app.errorhandler(Exception)
def handle_generic(e):
app.logger.exception('Unhandled exception')
return jsonify({'error': {'code': 'INTERNAL_ERROR', 'message': 'An unexpected error occurred'}}), 500Key rules:
{"error": {"code": "...", "message": "..."}}AppError classregister_error_handlers(app) is called inside create_app()Exception handler MUST log the error with app.logger.exception() before returning 500# Inside create_app() or on a blueprint
import uuid
from flask import g, request
@app.before_request
def before_request():
g.request_id = request.headers.get('X-Request-ID', str(uuid.uuid4()))
@app.after_request
def after_request(response):
response.headers['X-Request-ID'] = g.get('request_id', '')
return response # MUST return the response object
@app.teardown_appcontext
def teardown(exception):
db = g.pop('db', None)
if db is not None:
db.close()Key rules:
after_request MUST return the response object -- forgetting this silently breaks the appg (flask.g) for per-request state -- NOT module-level globalsteardown_appcontext runs even if an exception occurred -- use it for cleanup (DB connections, file handles)flask.g# Application context -- available with app.app_context()
# Use for: database init, CLI commands, background tasks
with app.app_context():
init_db() # OK: has access to current_app, g
# Request context -- available during HTTP request handling
# Use for: route handlers, before_request/after_request hooks
# Has access to: request, session, g, current_app
@orders_bp.route('/orders')
def list_orders():
# request and g are available here
passKey rules:
flask.request and flask.session are ONLY available inside a request contextflask.current_app and flask.g are available in BOTH request and application contextswith app.app_context(): when running code outside of a request (tests, CLI commands, init scripts)# WRONG: Hardcoded values, no way to override per environment
app.config['DATABASE_URL'] = 'sqlite:///production.db'
app.config['DEBUG'] = True # debug in production!# config.py
import os
class BaseConfig:
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-only-change-in-production')
DATABASE_PATH = os.getenv('DATABASE_PATH', 'data.db')
class DevelopmentConfig(BaseConfig):
DEBUG = True
class TestingConfig(BaseConfig):
TESTING = True
DATABASE_PATH = ':memory:'
class ProductionConfig(BaseConfig):
DEBUG = False
config_by_name = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
}Then in the factory:
def create_app(config_name=None):
app = Flask(__name__)
config_name = config_name or os.getenv('FLASK_ENV', 'development')
app.config.from_object(config_by_name[config_name])
...Key rules:
app.config.from_object() or app.config.from_mapping() -- not direct assignment scattered across filesTESTING = True should use in-memory databases and disable external servicesDEBUG must be False in production -- never set app.run(debug=True) in production code# Inside create_app()
import logging
if not app.debug:
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
formatter = logging.Formatter(
'%(asctime)s %(levelname)s %(name)s: %(message)s'
)
handler.setFormatter(formatter)
app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)Key rules:
app.logger (not print() or bare logging.getLogger()) inside the appcreate_app() -- not at module levelapp.logger.exception() in error handlers to capture tracebacks# Inside create_app()
import click
@app.cli.command('init-db')
def init_db_command():
"""Initialize the database."""
from app.db import init_db
init_db()
click.echo('Database initialized.')
@app.cli.command('seed')
@click.argument('count', default=10)
def seed_command(count):
"""Seed the database with sample data."""
from app.db import seed_db
seed_db(count)
click.echo(f'Seeded {count} records.')Key rules:
create_app() using @app.cli.command()click.echo() for output (not print())# tests/conftest.py
import pytest
from app import create_app
@pytest.fixture
def app():
app = create_app('testing')
yield app
@pytest.fixture
def client(app):
return app.test_client()
# tests/test_orders.py
def test_create_order(client):
response = client.post('/api/orders', json={
'customer_name': 'Alice',
'items': [{'menu_item_id': 1, 'quantity': 2}]
})
assert response.status_code == 201
data = response.get_json()
assert 'data' in data
def test_create_order_missing_name(client):
response = client.post('/api/orders', json={'items': []})
assert response.status_code == 400
data = response.get_json()
assert data['error']['code'] == 'VALIDATION_ERROR'Key rules:
create_app('testing') -- never import a global appapp.test_client() for HTTP-level testingcreate_app()) in app/__init__.py -- not module-level appextensions.py with no app, bound via init_app(app) in factoryapp/routes/)create_app() with url_prefixAppError base class with NotFoundError, ValidationError)register_error_handlers(app) in factory{"error": {"code": "...", "message": "..."}}Exception handler logs with app.logger.exception() before returning 500flask.g for per-request state -- no module-level request globalsafter_request hooks return the response objectteardown_appcontext for cleanup (DB connections)create_app() using app.logger@app.cli.command()create_app('testing') and app.test_client()app.run(debug=True) in production code