Skip to main content

Introduction

Framefox implements the Model-View-Controller (MVC) architectural pattern to separate concerns and organize your application code. This pattern divides your application into three interconnected components:
  • Models: Data structures and business logic (Entities/ORM)
  • Views: Presentation layer (Templates)
  • Controllers: Request handling and coordination

Controllers

Controllers handle HTTP requests, process data, and return responses. In Framefox, controllers are classes that extend AbstractController.

Basic Controller Structure

Controllers should be placed in the src/controller/ directory:
from framefox.core.controller.abstract_controller import AbstractController
from framefox.core.routing.decorator.route import Route

class UserController(AbstractController):
    def __init__(self):
        super().__init__()

    @Route("/users", "user.list", methods=["GET"])
    async def list_users(self):
        # Your logic here
        return self.render("user/list.html", {"users": users})

AbstractController Base Class

The AbstractController provides essential methods for controller functionality:
/home/daytona/workspace/source/framefox/core/controller/abstract_controller.py:15-24
class AbstractController:
    def __init__(self):
        """
        Initializes a new controller with dependency injection container and template renderer.
        Child controller can override this method
        to inject their own dependencies.
        """

        self._container = ServiceContainer()
        self.template_renderer = self._container.get_by_tag("core.templates.template_renderer")

Controller Methods

Rendering Templates

Render HTML templates with context data:
/home/daytona/workspace/source/framefox/core/controller/abstract_controller.py:67-86
def render(self, template_path, context=None):
    """
    Renders a template file with optional context data and returns an HTML response.

    Args:
        template_path (str): Path to the template file to render
        context (dict, optional): Data to pass to the template (default: None)

    Returns:
        HTMLResponse: The rendered template as HTML response
    """
    template_renderer = self._get_container().get_by_name("TemplateRenderer")
    self._last_rendered_template = template_path
    if context is None:
        context = {}
    try:
        content = template_renderer.render(template_path, context)
        return HTMLResponse(content=content)
    except Exception:
        raise
Example:
@Route("/profile", "user.profile", methods=["GET"])
async def profile(self):
    user = self.get_user()
    return self.render("user/profile.html", {
        "user": user,
        "title": "My Profile"
    })

JSON Responses

Return JSON data for API endpoints:
/home/daytona/workspace/source/framefox/core/controller/abstract_controller.py:88-99
def json(self, data: dict, status: int = 200):
    """
    Returns data as a JSON response with optional HTTP status code.

    Args:
        data (dict): The data to serialize as JSON
        status (int): HTTP status code for the response (default: 200)

    Returns:
        JSONResponse: The data serialized as JSON response
    """
    return JSONResponse(content=data, status_code=status)
Example:
@Route("/api/users", "api.users", methods=["GET"])
async def api_users(self):
    users = # fetch users from database
    return self.json({"users": users, "count": len(users)})

Redirects

Redirect users to different URLs:
/home/daytona/workspace/source/framefox/core/controller/abstract_controller.py:26-34
def redirect(self, location: str, code: int = 302):
    """
    Redirects the user to a specific URL with an optional HTTP status code.

    Args:
        location (str): The URL to redirect to
        code (int): HTTP status code for the redirect (default: 302)
    """
    return RedirectResponse(url=location, status_code=code)
Example:
@Route("/logout", "user.logout", methods=["POST"])
async def logout(self):
    # Clear session
    return self.redirect("/login")

Flash Messages

Add temporary messages to display on the next request:
/home/daytona/workspace/source/framefox/core/controller/abstract_controller.py:36-46
def flash(self, category: str, message: str):
    """
    Adds a flash message to the session for displaying on the next request.

    Args:
        category (str): The category/type of the flash message (e.g., 'success', 'error')
        message (str): The message content to display
    """
    session = self._get_container().get_by_name("Session")
    flash_bag = session.get_flash_bag()
    flash_bag.add(category, message)
Example:
@Route("/save", "user.save", methods=["POST"])
async def save(self):
    # Save user data
    self.flash("success", "Profile updated successfully!")
    return self.redirect("/profile")

URL Generation

Generate URLs from route names:
/home/daytona/workspace/source/framefox/core/controller/abstract_controller.py:53-65
def generate_url(self, route_name: str, **params):
    """
    Generates a URL for a named route with optional parameters.

    Args:
        route_name (str): The name of the route to generate URL for
        **params: Additional parameters to include in the URL

    Returns:
        str: The generated URL
    """
    router = self._get_container().get_by_name("Router")
    return router.url_path_for(route_name, **params)
Example:
profile_url = self.generate_url("user.profile", user_id=123)
# Returns: /users/123

Controller Example

Here’s a complete controller example from the framework:
/home/daytona/workspace/source/framefox/core/debug/profiler/profiler_controller.py:16-64
class ProfilerController(AbstractController):
    """
    Web profiler controller for Framefox.
    Handles request profiling information display.
    """

    def __init__(self):
        self.profiler = Profiler()

    @Route("/_profiler", "profiler.index", methods=["GET"])
    async def profiler_index(self, page: int = 1, limit: int = 50):
        profiles = self.profiler.list_profiles(limit=limit, page=page)
        total_count = len(self.profiler.list_profiles(limit=10000))

        return self.render(
            "profiler/index.html",
            {
                "profiles": profiles,
                "page": page,
                "limit": limit,
                "total": total_count,
                "page_count": (total_count + limit - 1) // limit,
            },
        )

    @Route("/_profiler/{token}", "profiler.detail", methods=["GET"])
    async def profiler_detail(self, token: str):
        profile = self.profiler.get_profile(token)
        return self.render("profiler/details.html", {"token": token, "profile": profile})

    @Route("/_profiler/{token}/{panel}", "profiler.panel", methods=["GET"])
    async def profiler_panel(self, token: str, panel: str):
        profile = self.profiler.get_profile(token)
        panel_data = profile.get(panel, {})

        template_context = {
            "token": token,
            "panel": panel,
            "data": panel_data,
            "profile": profile,
        }
        return self.render(f"profiler/panels/{panel}.html", template_context)

    @Route("/_profiler/{token}/json", "profiler.json", methods=["GET"])
    async def profiler_json(self, token: str):
        profile = self.profiler.get_profile(token)
        return JSONResponse(content=profile)

Views (Templates)

Views are Jinja2 templates located in your templates directory. They receive data from controllers and render HTML.

Template Structure

templates/
├── base.html           # Base layout
├── user/
│   ├── list.html      # User list view
│   ├── detail.html    # User detail view
│   └── form.html      # User form
└── components/
    └── navbar.html     # Reusable components

Template Example

{# templates/user/profile.html #}
{% extends "base.html" %}

{% block title %}{{ user.name }} - Profile{% endblock %}

{% block content %}
<div class="profile">
    <h1>{{ user.name }}</h1>
    <p>Email: {{ user.email }}</p>
    
    {% if user.is_admin %}
    <span class="badge">Admin</span>
    {% endif %}
</div>
{% endblock %}

Template Context

Controllers pass data to templates via the context dictionary:
return self.render("user/profile.html", {
    "user": user_object,
    "posts": user_posts,
    "count": len(user_posts)
})

Models (Entities)

Models represent your data structures and are typically ORM entities. In Framefox, you can use any ORM, but SQLAlchemy is commonly used.

Entity Example

from sqlalchemy import Column, Integer, String, Boolean
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50), unique=True, nullable=False)
    email = Column(String(120), unique=True, nullable=False)
    is_admin = Column(Boolean, default=False)
    
    def __repr__(self):
        return f'<User {self.username}>'

Using Models in Controllers

from framefox.core.orm.entity_manager_interface import EntityManagerInterface

class UserController(AbstractController):
    @Route("/users/{user_id}", "user.detail", methods=["GET"])
    async def detail(self, user_id: int, em: EntityManagerInterface):
        user = em.find(User, user_id)
        if not user:
            return self.json({"error": "User not found"}, status=404)
        
        return self.render("user/detail.html", {"user": user})
The EntityManagerInterface is automatically injected by the dependency injection system when you type-hint it in your controller method.

MVC Flow Diagram

Best Practices

Controller Guidelines

Controllers should only handle HTTP concerns. Move business logic to service classes.
# Good
class UserController(AbstractController):
    async def create(self, user_service: UserService):
        user = user_service.create_user(data)
        return self.json({"user": user})

# Bad
class UserController(AbstractController):
    async def create(self):
        # Too much logic in controller
        user = User()
        user.validate()
        user.hash_password()
        user.send_welcome_email()
        # ...
Inject services through type hints instead of manual instantiation.
# Good
@Route("/users", "users.list", methods=["GET"])
async def list(self, user_service: UserService):
    users = user_service.get_all()

# Bad
@Route("/users", "users.list", methods=["GET"])
async def list(self):
    user_service = UserService()  # Don't do this
    users = user_service.get_all()
Use a consistent naming scheme for routes (e.g., resource.action).
@Route("/users", "user.list", methods=["GET"])
@Route("/users/{id}", "user.detail", methods=["GET"])
@Route("/users/create", "user.create", methods=["POST"])
@Route("/users/{id}/edit", "user.edit", methods=["PUT"])
@Route("/users/{id}/delete", "user.delete", methods=["DELETE"])
Create separate controllers for different resources.
src/controller/
├── user_controller.py
├── post_controller.py
├── comment_controller.py
└── admin/
    ├── dashboard_controller.py
    └── settings_controller.py

Template Guidelines

  • Use template inheritance with {% extends %} for consistent layouts
  • Break down large templates into reusable components
  • Keep business logic out of templates
  • Use template filters for data formatting

Model Guidelines

  • Keep models focused on data structure
  • Use separate service classes for business logic
  • Define clear relationships between entities
  • Validate data at the model level

Next Steps

Routing

Learn about the @Route decorator and URL handling

Dependency Injection

Master service injection in controllers

Templates

Explore Jinja2 templating features

ORM

Work with databases and entities

Build docs developers (and LLMs) love