Skip to content

Web Controllers

Controllers are where your web routes actually live. They group related endpoints into a tidy class, handle the incoming request and return a response, usually a rendered view. You wire them into your app from your routes file, which itself is registered by your Package Provider.


Defining a Controller

A controller is a class decorated with @uvicore.controller() that defines a register(self, route) method. Inside register() you declare your endpoints, and you always return route at the end.

import uvicore
from uvicore.http import Request, response
from uvicore.http.routing import WebRouter, Controller

@uvicore.controller()
class Welcome(Controller):

    def register(self, route: WebRouter):
        """Register web routes for this controller."""

        @route.get('/', name='home')
        async def home(request: Request):
            return await response.View('wiki/welcome.j2', {
                'request': request,
                'message': 'Welcome to Uvicore!',
            })

        @route.get('/about', name='about')
        async def about(request: Request):
            return await response.View('wiki/about.j2', {'request': request})

        return route

Generate a controller skeleton from the CLI:

./uvicore gen controller welcome

Note

Every web handler receives request: Request and must pipe it into the view context. Views are rendered with the async await response.View(name, context) helper.

Tip

This page covers what goes inside a controller. For how controllers are loaded, prefixed, named and grouped, declaring route.controllers, including them and route.group(), see Web Routing.


Accessing the Request

Every route handler receives the request object, your gateway to everything about the incoming request:

Accessor What it gives you
request.user The current UserInfo (real even when anonymous, see Authentication)
request.query_params URL query string parameters
request.path_params Dynamic path segments
request.headers HTTP headers
request.cookies Cookie values
await request.json() Parsed JSON body
await request.form() Parsed form data
await request.body() Raw request body
request.url, request.method URL and method info
@route.get('/search', name='search')
async def search(request: Request):
    query = request.query_params.get('q', '')
    return await response.View('wiki/search.j2', {'request': request, 'query': query})

You can also let Uvicore inject typed parameters for you, query strings, path params and form fields, using the FastAPI-style param helpers from uvicore.http.params.

from uvicore.http.params import Form

@route.post('/search', name='search.post')
async def search(request: Request, q: str = Form(...)):
    ...

Returning Responses

The response namespace gives you a helper for every kind of response. Import the namespace, or the individual helpers.

from uvicore.http import response

View (HTML)

The async View() helper renders a Jinja template. Pass the template name and a context dict that includes request.

@route.get('/', name='home')
async def home(request: Request):
    return await response.View('wiki/home.j2', {'request': request, 'title': 'Home'})

Other Response Types

return response.HTML('<h1>Hello</h1>')
return response.Text('Plain text')
return response.JSON({'status': 'ok'})
return response.Redirect('/dashboard')
return response.File('/path/to/file.pdf')

See Views for everything response.View() can do, including status codes and headers.


Path Parameters

Declare dynamic segments with {param_name} and read them from request.path_params (or accept them as typed function arguments).

@route.get('/users/{user_id}', name='user.detail')
async def user_detail(request: Request, user_id: int):
    return await response.View('wiki/user.j2', {'request': request, 'user_id': user_id})

Named routes make URL generation a breeze in your templates:

<a href="{{ url('wiki.user.detail', user_id=123) }}">View User</a>

Controller-Wide Authorization

Need every route in a controller to require the same permissions? Declare class-level scopes (or auth / middleware) and they apply to the whole controller. See Authorization for the full picture.

@uvicore.controller()
class AdminPanel(Controller):
    scopes = ['authenticated', 'admin']

    def register(self, route: WebRouter):
        ...
        return route

Keeping Controllers Lean

Controllers should orchestrate, not implement. Pull real business logic into your ORM models, a service or a Job, and let the controller stay focused on turning a request into a response.

@route.get('/posts', name='posts')
async def posts(request: Request):
    from acme.wiki.models.post import Post
    posts = await Post.query().include('creator').order_by('id', 'DESC').get()
    return await response.View('wiki/posts.j2', {'request': request, 'posts': posts})

Testing Controllers

Web controllers are exercised with Uvicore's async test client.

import pytest

@pytest.mark.asyncio
async def test_home_page(app1, client):
    res = await client.get('/')
    assert res.status_code == 200
    assert b'Welcome' in res.content

Controller tips

  • Keep controllers lean, move business logic into models, services or jobs.
  • Always pipe request into your view context, templates need it for the user and helpers.
  • Use named routes so refactoring URLs doesn't break your templates.
  • Group related endpoints in one controller, and put shared auth at the class or group level.