Overview
Flask supports class-based views as an alternative to function-based views. They provide better code organization, reusability, and support for common patterns like REST APIs.Basic Class-Based Views
The View Class
from flask import Flask, render_template
from flask.views import View
app = Flask(__name__)
class ShowUsers(View):
def dispatch_request(self):
users = User.query.all()
return render_template('users.html', users=users)
app.add_url_rule('/users', view_func=ShowUsers.as_view('show_users'))
Accepting URL Parameters
class ShowUser(View):
def dispatch_request(self, user_id):
user = User.query.get_or_404(user_id)
return render_template('user.html', user=user)
app.add_url_rule('/users/<int:user_id>', view_func=ShowUser.as_view('show_user'))
MethodView for REST APIs
MethodView automatically dispatches to methods based on HTTP request method:
from flask.views import MethodView
from flask import jsonify, request
class UserAPI(MethodView):
"""REST API for users."""
def get(self, user_id):
"""Get a user or list all users."""
if user_id is None:
# List all users
users = User.query.all()
return jsonify([u.to_dict() for u in users])
else:
# Get specific user
user = User.query.get_or_404(user_id)
return jsonify(user.to_dict())
def post(self):
"""Create a new user."""
data = request.get_json()
user = User(**data)
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict()), 201
def put(self, user_id):
"""Update a user."""
user = User.query.get_or_404(user_id)
data = request.get_json()
for key, value in data.items():
setattr(user, key, value)
db.session.commit()
return jsonify(user.to_dict())
def delete(self, user_id):
"""Delete a user."""
user = User.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return '', 204
# Register view for list and detail
user_view = UserAPI.as_view('user_api')
app.add_url_rule('/users', defaults={'user_id': None},
view_func=user_view, methods=['GET'])
app.add_url_rule('/users', view_func=user_view, methods=['POST'])
app.add_url_rule('/users/<int:user_id>', view_func=user_view,
methods=['GET', 'PUT', 'DELETE'])
View Decorators
Applying Decorators
from functools import wraps
from flask import abort, g
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not g.user:
abort(401)
return f(*args, **kwargs)
return decorated_function
class ProtectedView(View):
decorators = [login_required]
def dispatch_request(self):
return render_template('protected.html')
Multiple Decorators
from flask_limiter import Limiter
def admin_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if not g.user.is_admin:
abort(403)
return f(*args, **kwargs)
return decorated
class AdminView(MethodView):
decorators = [login_required, admin_required]
def get(self):
return render_template('admin.html')
def post(self):
# Handle admin action
return redirect(url_for('admin'))
Decorators in the
decorators list are applied in order, from bottom to top (like stacking @decorator syntax).View Initialization
Per-Request Initialization (Default)
class MyView(View):
# Default: create new instance per request
init_every_request = True
def __init__(self):
self.timestamp = datetime.now()
def dispatch_request(self):
# Each request gets a fresh timestamp
return f'Created at {self.timestamp}'
Shared Instance Across Requests
class CachedView(View):
# Reuse same instance for all requests
init_every_request = False
def __init__(self):
# This runs once when view is registered
self.cache = {}
def dispatch_request(self):
# Same cache instance for all requests
# Don't store request-specific data on self!
return self.cache.get('data', 'No data')
When
init_every_request = False, never store request-specific data on self. Use flask.g instead.Passing Arguments to Views
class UserList(View):
def __init__(self, template_name):
self.template_name = template_name
def dispatch_request(self):
users = User.query.all()
return render_template(self.template_name, users=users)
# Pass arguments when registering
app.add_url_rule(
'/users',
view_func=UserList.as_view('users', template_name='users.html')
)
app.add_url_rule(
'/admin/users',
view_func=UserList.as_view('admin_users', template_name='admin/users.html')
)
Method Hints
Specifying Allowed Methods
class UserAPI(MethodView):
# Explicitly define allowed methods
methods = ['GET', 'POST']
def get(self):
return jsonify({'users': []})
def post(self):
return jsonify({'created': True}), 201
For
MethodView, the methods attribute is automatically set based on defined methods. You only need to set it explicitly if you want to restrict methods.Common Patterns
CRUD Resource View
class ResourceView(MethodView):
"""Base class for CRUD resources."""
def __init__(self, model):
self.model = model
def get(self, id):
if id is None:
items = self.model.query.all()
return jsonify([item.to_dict() for item in items])
item = self.model.query.get_or_404(id)
return jsonify(item.to_dict())
def post(self):
data = request.get_json()
item = self.model(**data)
db.session.add(item)
db.session.commit()
return jsonify(item.to_dict()), 201
def put(self, id):
item = self.model.query.get_or_404(id)
data = request.get_json()
for key, value in data.items():
if hasattr(item, key):
setattr(item, key, value)
db.session.commit()
return jsonify(item.to_dict())
def delete(self, id):
item = self.model.query.get_or_404(id)
db.session.delete(item)
db.session.commit()
return '', 204
# Register for different models
def register_api(app, model, name, pk='int:id'):
"""Register a CRUD API for a model."""
view = ResourceView.as_view(f'{name}_api', model=model)
app.add_url_rule(
f'/{name}',
defaults={'id': None},
view_func=view,
methods=['GET']
)
app.add_url_rule(
f'/{name}',
view_func=view,
methods=['POST']
)
app.add_url_rule(
f'/{name}/<{pk}>',
view_func=view,
methods=['GET', 'PUT', 'DELETE']
)
# Use with different models
register_api(app, User, 'users')
register_api(app, Post, 'posts')
register_api(app, Comment, 'comments')
Pagination View
class PaginatedView(MethodView):
"""View with pagination support."""
per_page = 20
def get_query(self):
"""Override to customize query."""
raise NotImplementedError
def get(self):
page = request.args.get('page', 1, type=int)
query = self.get_query()
pagination = query.paginate(
page=page,
per_page=self.per_page,
error_out=False
)
return jsonify({
'items': [item.to_dict() for item in pagination.items],
'page': page,
'per_page': self.per_page,
'total': pagination.total,
'pages': pagination.pages
})
class UserList(PaginatedView):
per_page = 50
def get_query(self):
return User.query.order_by(User.created_at.desc())
Form View
from flask_wtf import FlaskForm
class FormView(MethodView):
"""Base view for handling forms."""
form_class = None
template_name = None
success_url = None
def get_form(self):
return self.form_class()
def get_context(self, form):
return {'form': form}
def get(self):
form = self.get_form()
context = self.get_context(form)
return render_template(self.template_name, **context)
def post(self):
form = self.get_form()
if form.validate_on_submit():
self.form_valid(form)
return redirect(self.success_url)
context = self.get_context(form)
return render_template(self.template_name, **context)
def form_valid(self, form):
"""Override to handle valid form submission."""
raise NotImplementedError
class UserCreateView(FormView):
form_class = UserForm
template_name = 'users/create.html'
success_url = '/users'
def form_valid(self, form):
user = User(**form.data)
db.session.add(user)
db.session.commit()
flash('User created successfully!')
Async Views
Class-based views support async methods:class AsyncUserAPI(MethodView):
async def get(self, user_id):
"""Async GET handler."""
user = await User.get(user_id)
return jsonify(user.to_dict())
async def post(self):
"""Async POST handler."""
data = await request.get_json()
user = await User.create(**data)
return jsonify(user.to_dict()), 201
Testing Class-Based Views
def test_user_list(client):
"""Test UserAPI.get() without user_id."""
response = client.get('/users')
assert response.status_code == 200
data = response.get_json()
assert isinstance(data, list)
def test_user_create(client):
"""Test UserAPI.post()."""
response = client.post('/users', json={
'username': 'alice',
'email': '[email protected]'
})
assert response.status_code == 201
data = response.get_json()
assert data['username'] == 'alice'
def test_user_update(client):
"""Test UserAPI.put()."""
response = client.put('/users/1', json={
'email': '[email protected]'
})
assert response.status_code == 200
data = response.get_json()
assert data['email'] == '[email protected]'
Best Practices
1. Use MethodView for APIs
# Good: Clear separation of HTTP methods
class UserAPI(MethodView):
def get(self, user_id):
pass
def post(self):
pass
# Less ideal: Function view with if/else
@app.route('/users', methods=['GET', 'POST'])
def users():
if request.method == 'POST':
# ...
else:
# ...
2. Keep Views Focused
# Good: Single responsibility
class UserList(MethodView):
def get(self):
users = User.query.all()
return jsonify([u.to_dict() for u in users])
# Bad: Too much logic in view
class UserList(MethodView):
def get(self):
# Complex filtering logic
# Business logic
# Serialization logic
# All in one method
3. Use Inheritance for Common Patterns
class AuthenticatedView(MethodView):
decorators = [login_required]
class AdminView(AuthenticatedView):
decorators = AuthenticatedView.decorators + [admin_required]
class UserManagement(AdminView):
def get(self):
# Automatically requires login and admin
return render_template('admin/users.html')
4. Document View Behavior
class UserAPI(MethodView):
"""REST API for user management.
Endpoints:
GET /users - List all users
GET /users/<id> - Get user by ID
POST /users - Create new user
PUT /users/<id> - Update user
DELETE /users/<id> - Delete user
Authentication:
All endpoints require authentication via API key.
"""
def get(self, user_id):
"""Get user(s).
Args:
user_id: User ID or None for list
Returns:
JSON response with user data
"""
pass
