Skip to main content

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

Build docs developers (and LLMs) love