Routes & Controllers

Chapter 5: Handling HTTP requests and returning responses.

Controllers are the traffic cops of your app. They receive HTTP requests, call your models to get or save data, and return responses (usually HTML pages). Let's see how to build them.

What is a Controller?

A controller is a Python file with Flask routes that handle different URLs. When a user visits your app's URL, Flask finds the matching route and runs your code.

# controllers/main.py
from flask import Blueprint, render_template

bp = Blueprint('main', __name__, url_prefix='/tasks')

@bp.route('/')
def index():
    # This runs when user visits your app
    return render_template('tasks/desktop/index.html')

URL Structure for Marketplace Apps

Marketplace apps use a special URL pattern that includes the app's mappid:

/m/{mappid}/{slug}
 │    │        │
 │    │        └── From your main_route in __manifest__.py
 │    └── Your app's unique 6-char identifier
 └── Marketplace prefix

For example, if your manifest has:

manifest = {
    "mappid": "x7k2m9",
    "main_route": "/tasks",
    ...
}

Your app will be accessible at /m/x7k2m9/tasks.

Route registration is automatic. You define routes with your url_prefix (e.g., /tasks), and sparQ automatically prepends /m/{mappid}/ when registering them. You don't need to handle the mappid in your code.

Creating a Controller

Controllers go in the controllers/ folder. Here's a complete example:

# controllers/main.py
from flask import Blueprint, render_template, request, redirect, url_for, flash
from ..models.task import Task

bp = Blueprint('main', __name__, url_prefix='/tasks')

@bp.route('/')
def index():
    """Show all tasks."""
    tasks = Task.get_all()
    return render_template('tasks/desktop/index.html', tasks=tasks)

The Blueprint groups related routes together. The url_prefix means all routes in this file start with /tasks.

Common Route Patterns

Most apps need these standard CRUD (Create, Read, Update, Delete) routes:

List View

@bp.route('/')
def index():
    tasks = Task.get_all()
    return render_template('tasks/desktop/index.html', tasks=tasks)

Detail View

@bp.route('/<int:id>')
def show(id):
    task = Task.get_by_id(id)
    if not task:
        abort(404)
    return render_template('tasks/desktop/show.html', task=task)

Create Form

@bp.route('/new', methods=['GET', 'POST'])
def create():
    if request.method == 'POST':
        title = request.form.get('title')
        Task.create(title=title)
        flash('Task created!', 'success')
        return redirect(url_for('tasks.main.index'))
    return render_template('tasks/desktop/form.html')

Edit Form

@bp.route('/<int:id>/edit', methods=['GET', 'POST'])
def edit(id):
    task = Task.get_by_id(id)
    if request.method == 'POST':
        task.update(title=request.form.get('title'))
        flash('Task updated!', 'success')
        return redirect(url_for('tasks.main.show', id=id))
    return render_template('tasks/desktop/form.html', task=task)

Delete

@bp.route('/<int:id>/delete', methods=['POST'])
def delete(id):
    task = Task.get_by_id(id)
    task.delete()
    flash('Task deleted!', 'success')
    return redirect(url_for('tasks.main.index'))

Getting Form Data

Flask provides the request object to access form data:

from flask import request

# Form data (POST)
title = request.form.get('title')
done = request.form.get('done') == 'on'  # Checkbox

# Query parameters (GET)
search = request.args.get('q', '')
page = request.args.get('page', 1, type=int)

Redirects and Flash Messages

After creating or updating data, redirect the user and show a message:

from flask import redirect, url_for, flash

@bp.route('/new', methods=['POST'])
def create():
    Task.create(title=request.form.get('title'))
    flash('Task created successfully!', 'success')
    return redirect(url_for('tasks.main.index'))

Flash message types: success, error, warning, info

Authentication

Protect routes so only logged-in users can access them:

from system.auth import login_required, current_user

@bp.route('/dashboard')
@login_required
def dashboard():
    # Only runs if user is logged in
    user_tasks = Task.query.filter_by(user_id=current_user.id).all()
    return render_template('tasks/desktop/dashboard.html', tasks=user_tasks)

The @login_required decorator redirects anonymous users to the login page.

HTMX Endpoints

HTMX lets you update parts of a page without a full reload. Return just the HTML fragment, not the whole page:

@bp.route('/<int:id>/toggle', methods=['POST'])
def toggle(id):
    task = Task.get_by_id(id)
    task.done = not task.done
    db.session.commit()
    # Return just the updated task row
    return render_template('tasks/desktop/partials/task_row.html', task=task)

And in your template:

<tr hx-post="/tasks/{{ task.id }}/toggle"
    hx-swap="outerHTML">
    <td>{{ task.title }}</td>
    <td>{{ "Done" if task.done else "Pending" }}</td>
</tr>
Image: Animation showing HTMX toggling a task without page reload

Route Naming

Use url_for() to generate URLs instead of hardcoding them:

# In Python
url_for('tasks.main.index')           # /tasks/
url_for('tasks.main.show', id=5)      # /tasks/5
url_for('tasks.main.edit', id=5)      # /tasks/5/edit
<!-- In templates -->
<a href="{{ url_for('tasks.main.show', id=task.id) }}">View</a>

The naming pattern is: app_name.blueprint_name.function_name

Device-Aware Rendering

sparQ provides render_device_template() as the recommended way to render templates. It automatically serves the mobile version of a template on mobile devices, falling back to the desktop version if no mobile template exists.

from system.device.template import render_device_template

@bp.route('/')
def index():
    tasks = Task.get_all()
    return render_device_template('tasks/desktop/index.html', tasks=tasks)

Use render_device_template instead of Flask's render_template in all your controllers. You can test mobile rendering by appending ?device=mobile to any URL.

Key Takeaways