Skip to content

HTTP controllers

An HTTP controller organizes the logic of one or several endpoints. When the router finds a match and middleware allow execution to continue, Orionis constructs the controller through the container, invokes its action and processes the returned response.

Keeping actions in controllers lets route files concentrate on endpoint declarations while services handle business logic.

Resource and Invokable are two controller structures. --api is an independent option that changes response annotations and can be used with either structure.

From the project root, with its Python environment active, use the direct command:

Terminal window
orionis make:http-controller ExampleController

Or its alternative through the local reactor file:

Terminal window
python -B reactor make:http-controller ExampleController

The syntax is make:http-controller name [--invoke] [--api]:

Argument or optionRequiredDefaultEffect
nameYesNo valueName used to construct the file and class.
--invokeNoFalseGenerate only the __call__ action instead of the seven Resource actions.
--apiNoFalseImport JSONResponse and use it in return annotations and docstrings.

-h or --help display help. The --invoke and --api options can be combined.

The default destination is app/http/controllers, obtained from the app_http_controllers application path. Customizing that path changes the generator’s destination.

nameGenerated classProject-relative file
ExampleControllerExampleControllerapp/http/controllers/examplecontroller.py
UserControllerUserControllerapp/http/controllers/usercontroller.py
user_controller or userUserControllerapp/http/controllers/user_controller.py
admin/userUserControllerapp/http/controllers/admin/user_controller.py

The filename is lowercase and gains _controller when the suffix is missing. The class is formed from underscore-separated segments and gains Controller when missing. PascalCase words are not separated with underscores: UserController produces usercontroller.py, not user_controller.py.

Subdirectories with / or \ are supported, normalized to lowercase, as is a final extension such as .py. Directories are created automatically. Absolute paths, .. segments and names containing hyphens are rejected.

The generator does not overwrite existing files and provides no --force option. The following variants are alternatives: use different names to keep several, or edit the file already generated.

A Resource Controller groups actions related to a resource. Use it when listing, forms, creation, updates and deletion belong to the same entity.

This is the default structure:

Terminal window
orionis make:http-controller UserController

Local alternative:

Terminal window
python -B reactor make:http-controller UserController

The app/http/controllers/usercontroller.py file contains:

from orionis.http import HttpResponse
from orionis.http.base import BaseController
class UserController(BaseController):
"""
Handle HTTP requests through asynchronous controller actions.
Resource actions are generated by default. Use the invokable option to
generate only ``__call__`` and the API option to annotate JSON responses.
"""
async def index(self) -> HttpResponse:
"""Display a listing of the resource.
Returns
-------
HttpResponse
Response produced when this action is implemented.
"""
async def create(self) -> HttpResponse:
"""Show the form for creating a resource.
Returns
-------
HttpResponse
Response produced when this action is implemented.
"""
async def store(self) -> HttpResponse:
"""Store a newly created resource.
Returns
-------
HttpResponse
Response produced when this action is implemented.
"""
async def show(self) -> HttpResponse:
"""Display the specified resource.
Returns
-------
HttpResponse
Response produced when this action is implemented.
"""
async def edit(self) -> HttpResponse:
"""Show the form for editing the resource.
Returns
-------
HttpResponse
Response produced when this action is implemented.
"""
async def update(self) -> HttpResponse:
"""Update the specified resource.
Returns
-------
HttpResponse
Response produced when this action is implemented.
"""
async def destroy(self) -> HttpResponse:
"""Remove the specified resource.
Returns
-------
HttpResponse
Response produced when this action is implemented.
"""

BaseController, imported from orionis.http.base, is an empty marker base class: it does not implement CRUD, validation, authorization or data access. HttpResponse is the framework’s type alias for Response.

All actions are asynchronous, receive only self and are annotated with HttpResponse. The generator does not automatically add Request, identifiers or dependencies.

ActionPurpose described by the template
indexList the resource.
createShow the creation form.
storeStore a new resource.
showShow a particular resource.
editShow the editing form.
updateUpdate a particular resource.
destroyRemove a particular resource.

These names organize responsibilities; their bodies do not perform those operations until implemented.

Implementing actions and registering routes

Section titled “Implementing actions and registering routes”

To try two read actions, replace the example file with this minimal implementation:

from orionis.http import HttpResponse, response
from orionis.http.base import BaseController
class UserController(BaseController):
async def index(self) -> HttpResponse:
return response.json({"users": []})
async def show(self, user_id: int) -> HttpResponse:
return response.json({"id": user_id})

In the file registered as API, usually routes/api.py:

from app.http.controllers.usercontroller import UserController
from orionis.support.facades import Route
Route.get("/users", [UserController, "index"])
Route.get("/users/{user_id:int}", [UserController, "show"])

GET /users returns {"users": []}; GET /users/42 returns {"id": 42}. The second action adds user_id to its signature to receive the route parameter.

The [Class, "action"] pair identifies the method without executing it. A tuple is also supported. Pass the class without instantiating it.

There is no automatic Resource route registration. The generator does not associate HTTP verbs with actions: this example chooses GET for both reads. Declare other routes explicitly with the HTTP methods appropriate for your implemented actions. See Routing.

An Invokable Controller concentrates one operation in __call__. Use it for an endpoint with a specific responsibility, without grouping several actions in the class.

Terminal window
orionis make:http-controller UserController --invoke

Local alternative:

Terminal window
python -B reactor make:http-controller UserController --invoke

In this variant, app/http/controllers/usercontroller.py contains only one action:

from orionis.http import HttpResponse
from orionis.http.base import BaseController
class UserController(BaseController):
"""
Handle HTTP requests through asynchronous controller actions.
Resource actions are generated by default. Use the invokable option to
generate only ``__call__`` and the API option to annotate JSON responses.
"""
async def __call__(self) -> HttpResponse:
"""Handle the request for this controller.
Returns
-------
HttpResponse
Response produced when this action is implemented.
"""

__call__ is the method the router selects when registering an invokable class directly. The skeleton receives only self; route parameters and dependencies can be added as for any action.

Implementing and registering the single action

Section titled “Implementing and registering the single action”

Replace the skeleton with an implementation that returns a response:

from orionis.http import HttpResponse, response
from orionis.http.base import BaseController
class UserController(BaseController):
async def __call__(self) -> HttpResponse:
return response.text("User endpoint ready")

In the route file:

from app.http.controllers.usercontroller import UserController
from orionis.support.facades import Route
Route.get("/user-summary", UserController)

GET /user-summary executes __call__() and returns the text with status 200. [UserController, "__call__"] is also supported, although passing the class expresses the same intent more briefly. The router does not accept an instance or bound method as an action.

--api replaces the HttpResponse import with JSONResponse, changes return annotations and the type mentioned in docstrings. It adds no response code, serialization, routes or persistence.

The structure still depends on --invoke: without that option, all seven Resource actions remain, including create and edit; with it, only __call__ is generated.

Terminal window
orionis make:http-controller UserController --api

Local alternative:

Terminal window
python -B reactor make:http-controller UserController --api

The complete generated code is:

from orionis.http import JSONResponse
from orionis.http.base import BaseController
class UserController(BaseController):
"""
Handle HTTP requests through asynchronous controller actions.
Resource actions are generated by default. Use the invokable option to
generate only ``__call__`` and the API option to annotate JSON responses.
"""
async def index(self) -> JSONResponse:
"""Display a listing of the resource.
Returns
-------
JSONResponse
Response produced when this action is implemented.
"""
async def create(self) -> JSONResponse:
"""Show the form for creating a resource.
Returns
-------
JSONResponse
Response produced when this action is implemented.
"""
async def store(self) -> JSONResponse:
"""Store a newly created resource.
Returns
-------
JSONResponse
Response produced when this action is implemented.
"""
async def show(self) -> JSONResponse:
"""Display the specified resource.
Returns
-------
JSONResponse
Response produced when this action is implemented.
"""
async def edit(self) -> JSONResponse:
"""Show the form for editing the resource.
Returns
-------
JSONResponse
Response produced when this action is implemented.
"""
async def update(self) -> JSONResponse:
"""Update the specified resource.
Returns
-------
JSONResponse
Response produced when this action is implemented.
"""
async def destroy(self) -> JSONResponse:
"""Remove the specified resource.
Returns
-------
JSONResponse
Response produced when this action is implemented.
"""

To implement read actions with explicit JSON responses, edit the file:

from orionis.http import JSONResponse, response
from orionis.http.base import BaseController
class UserController(BaseController):
async def index(self) -> JSONResponse:
return response.json({"users": []})
async def show(self, user_id: int) -> JSONResponse:
return response.json({"id": user_id})

Register them in routes/api.py using the same class and action pairs:

from app.http.controllers.usercontroller import UserController
from orionis.support.facades import Route
Route.get("/users", [UserController, "index"])
Route.get("/users/{user_id:int}", [UserController, "show"])

The options can be combined without restrictions between them:

Terminal window
orionis make:http-controller UserController --invoke --api

Local alternative:

Terminal window
python -B reactor make:http-controller UserController --invoke --api

The complete result is:

from orionis.http import JSONResponse
from orionis.http.base import BaseController
class UserController(BaseController):
"""
Handle HTTP requests through asynchronous controller actions.
Resource actions are generated by default. Use the invokable option to
generate only ``__call__`` and the API option to annotate JSON responses.
"""
async def __call__(self) -> JSONResponse:
"""Handle the request for this controller.
Returns
-------
JSONResponse
Response produced when this action is implemented.
"""

A functional implementation of that single action would be:

from orionis.http import JSONResponse, response
from orionis.http.base import BaseController
class UserController(BaseController):
async def __call__(self) -> JSONResponse:
return response.json({"status": "ready"})

In routes/api.py:

from app.http.controllers.usercontroller import UserController
from orionis.support.facades import Route
Route.get("/user-summary", UserController)

This endpoint returns JSON with status 200. Resource and Invokable examples are alternatives; register a single action per HTTP verb and path combination.

A JSONResponse annotation does not itself convert the returned value. The examples construct responses through response.json(...).

Separately, the kernel accepts dict or msgspec.Struct results and converts them into JSONResponse with status 200, independently of --api and the annotation. It does not convert arbitrary values: construct a text response with response.text(...), for example. See HTTP responses.

API or WEB context comes from app.withRouting(...) configuration, rather than --api, the controller name or its annotation. A controller generated without --api can also return JSON, as the first Resource example does. See Separating API and WEB routes.

Arguments in the table follow orionis make:http-controller or python -B reactor make:http-controller. The type is the expected return once the action is implemented, rather than a response already created by the skeleton.

StructureArgumentsGenerated actionsReturn annotationTypical useRoute registration
ResourceUserControllerAll seven resource actions.HttpResponseSeveral operations for one resource.[UserController, "index"]
InvokableUserController --invoke__call__HttpResponseOne specific operation.UserController
Resource with APIUserController --apiThe same seven actions.JSONResponseSeveral operations returning JSON.[UserController, "index"]
Invokable with APIUserController --invoke --api__call__JSONResponseOne operation returning JSON.UserController

When handling a route, Orionis constructs the controller class and resolves its constructor’s annotated dependencies. It then invokes the action, supplying route parameters by name and resolving its remaining dependencies. Preparing metadata during startup does not execute ordinary constructors or actions.

This example uses two concrete services without dependencies, which the container can construct automatically. Save app/services/user_label_service.py:

import logging
class UserLabelService:
def label(self, user_id: int) -> str:
return f"User {user_id}"
class UserAuditService:
def record_view(self, user_id: int, path: str) -> None:
logging.getLogger(__name__).info("Viewing user %s at %s", user_id, path)

In app/http/controllers/show_user_controller.py:

from app.services.user_label_service import UserAuditService, UserLabelService
from orionis.http import JSONResponse, Request, response
from orionis.http.base import BaseController
class ShowUserController(BaseController):
def __init__(self, labels: UserLabelService) -> None:
self.labels = labels
async def __call__(
self,
request: Request,
user_id: int,
audit: UserAuditService,
) -> JSONResponse:
audit.record_view(user_id, request.path)
return response.json({
"id": user_id,
"label": self.labels.label(user_id),
"path": request.path,
})

Register the class in routes/api.py, replacing the equivalent declaration if already using that path:

from app.http.controllers.show_user_controller import ShowUserController
from orionis.support.facades import Route
Route.get("/users/{user_id:int}", ShowUserController)

GET /users/42 returns {"id": 42, "label": "User 42", "path": "/users/42"}.

  • labels: UserLabelService lets the container construct and inject the service into the constructor. Abstract services or contracts need their corresponding binding.
  • audit: UserAuditService resolves when invoking this action. The client does not send it in the URL or payload: it is a server dependency.
  • request: Request obtains the current request registered in the HTTP scope; you do not construct it manually.
  • user_id arrives as an integer through the {user_id:int} converter. Accept that name in the action, including when receiving Request.
  • Annotations identify dependencies, while the converter declared in the route determines segment conversion. Adding : int only to the signature does not change the route pattern.
  • Response is the action’s return value; the controller does not automatically receive a pre-created response.

Use the constructor for services shared by several actions of a class and method parameters for services needed by one action. The same mechanism applies to Resource actions: if you rename __call__ to show, register [ShowUserController, "show"]. Constructor dependencies resolve first, followed by action arguments; route values are supplied to the action, rather than the constructor.

This example needs no bindings because both services are concrete and auto-constructible. To share the same instance between resolutions, configure its lifetime. Injection happens when Orionis constructs and invokes the controller; a direct Python call such as controller.show(...) requires you to supply its arguments.

Group controllers within app/http/controllers by responsibility or domain as the project grows; for example, admin/user generates a class inside admin. Use consistent names, Resource for a resource’s actions and Invokable for specific operations. These are organizational recommendations, rather than router restrictions.

Keep HTTP coordination in controllers and delegate business logic to services. The base class does not perform database operations or automatically implement generated actions.

Middleware execute before invocation and may prevent it. If an action fails, HTTP layers and the kernel delegate handling to the exception system. For formats, headers, cookies or downloads, see Responses.