Controllers

Controllers

On this page

A controller action is a plain async fn. There is no base class to extend, and no Controller trait to implement.

// src/app/http/controllers/home_controller.rs
use rainier_framework::prelude::*;

pub async fn index() -> Result<Response> {
    let html = View::instance().render("home", &json!({ "title": "Hello" }))?;
    Ok(Response::html(html))
}
router.get("/", HomeController::index).name("home");

Grouping them in a module named after the controller is a convention, not a requirement — it is what makes home_controller::index read like HomeController@index.

What an action may return

Anything implementing IntoResponse:

Return typeBecomes
Responseitself
&'static str, String200 text/plain
Html<T>200 text/html
Json<T>, serde_json::Value200 application/json
Redirect302/301/303/307
StatusCodethat status, empty body
()200, empty body
Body200 with that body
(StatusCode, T)T's response, with that status
Option<T>T's response, or 404
Result<T, E> where E: Into<Error>T's response, or the error's

So the shortest useful action is:

async fn health() -> &'static str { "ok" }

and the usual one is Result<Response>, because ? then works on anything returning Error.

Extractors

An action's parameters say what it needs. Each must implement FromRequest, and they run in order before the body of the action.

pub async fn store(
    request: Req,
    Validated(input): Validated<StorePostRequest>,
) -> Result<Response> {
    // `request` is the whole request; `input` is authorised, validated,
    // and contains only the fields the rules named.
}
ExtractorYieldsFails with
Req (Arc<Request>)the whole requestnever
Json<T>T from a JSON body400
Form<T>T from a urlencoded body400
Query<T>T from the query string400
Path<T>T from route parameters400
Header<K>one header's value400 if absent
Bearerthe Authorization: Bearer token401
RawBodythe raw Bytesnever
Files<K>uploads for a fieldnever (empty vec)
Validated<T>a validated contract403 / 422
Option<T>Some/None instead of failingnever
Result<T>the failure, for you to handlenever

Up to a reasonable arity, in any order:

async fn update(
    Path(id): Path<u64>,
    Bearer(token): Bearer,
    Json(body): Json<UpdatePost>,
) -> Result<Response> { … }

Header<K> and Files<K> need a name

Rust cannot yet take a &'static str as a const generic parameter, so the name arrives as a marker type:

use rainier_framework::http::extract::Header;
use rainier_framework::http::static_name;

static_name!(XTenant, "x-tenant");

async fn show(Header(tenant, ..): Header<XTenant>) -> Result<Response> { … }

static_name! writes the marker type and its StaticName impl. The same marker works for Files<K>.

Failing softly

Wrap in Option or Result when a missing value is not an error:

async fn index(page: Option<Query<Page>>) -> Result<Response> {
    let page = page.map(|Query(p)| p).unwrap_or_default();
    …
}

Getting at services

Handler parameters are for request-derived values. Services come from the container:

pub(crate) fn resolve<T: Send + Sync + 'static>() -> Result<Arc<T>> {
    rainier_framework::container::facade_application().resolve::<T>()
}

pub async fn index() -> Result<Response> {
    let posts = resolve::<PostRepository>()?;
    Ok(Response::json(&posts.all().await?))
}

That three-line helper at the bottom of your controllers module is the sample project's approach, and it is the one to copy. Injecting services as handler parameters would mean the router knowing how to resolve them, which is a dependency the architecture deliberately does not have.

Reading the authenticated user

The auth middleware puts an AuthenticatedUser<U> in the request's extensions:

pub(crate) fn current_user(request: &Request) -> Result<User> {
    request
        .extension::<AuthenticatedUser<User>>()
        .map(|user| user.get().clone())
        .ok_or_else(|| Error::unauthenticated("Unauthenticated."))
}

A 401 rather than a .expect(): moving the route out from behind auth should be a wrong answer, not a crash.

A realistic action

From the sample project — note what it does not do:

/// `POST /api/posts/{post}/publish` — behind `auth:api`.
pub async fn publish(request: Req) -> Result<Response> {
    let author = current_user(&request)?;
    let slug = route_param(&request, "post")?;
    let posts = resolve::<PostRepository>()?;

    let mut post = posts.first_by("slug", slug.into()).await?
        .ok_or_else(|| Error::not_found("No Post matches the given key."))?;

    // Authorisation is a policy, not an `if` buried in the controller.
    PostPolicy::gate().authorize("posts.publish", &author, Some(&post))?;

    if post.published {
        // Publishing twice must not send a second notification.
        return Ok(Response::json(&post));
    }

    post.published = true;
    posts.update(&post).await?;

    Event::instance().dispatch(PostPublished { post: post.clone() }).await?;
    Queue::instance().dispatch(NotifyAuthor { post_id: post.id }).await?;

    Ok(Response::json(&post))
}

It does not send mail. It writes the row, raises an event and queues a job, and the response goes out immediately. Whether the author's notification succeeds is the worker's problem, not this request's.

Resource controllers

For the seven RESTful actions, implement ResourceController and register it in one line:

use rainier_framework::prelude::*;

pub struct PostController;

#[async_trait]
impl ResourceController for PostController {
    async fn index(&self, _request: Request) -> Response {
        Response::text("every post")
    }

    async fn show(&self, request: Request) -> Response {
        Response::text(format!("post {}", request.route_param("post").unwrap_or("?")))
    }
}
router.resource("posts", Arc::new(PostController));

Every action has a default that answers 405, so you implement only the ones the resource supports. See Routing for the table of URIs and names.

Resource actions take Request by value and return Response — no extractors and no Result, because the trait has to be object-safe. For anything richer, use plain functions and declare the routes yourself.

Middleware on a controller

Middleware scoped to some of a controller's actions, with the actions as an enum and the middleware as a value:

impl ResourceController for PostController {
    async fn index(&self, request: Request) -> Response { … }
    async fn store(&self, request: Request) -> Response { … }
    async fn destroy(&self, request: Request) -> Response { … }

    fn middleware(&self) -> ControllerMiddleware {
        ControllerMiddleware::new()
            .except(
                [ResourceAction::Index, ResourceAction::Show],
                Authenticate::<User>::resolved(),
            )
            .only([ResourceAction::Destroy], RequireRole::Admin)
    }
}
MethodApplies to
always(m)every action
only([actions], m)just those
except([actions], m)everything else

Prefer except. An action added next year arrives guarded; an only list leaves it public until somebody remembers to add it. That is the whole difference between the two, and it is worth the habit.

Declaring the guard here rather than in the route file puts the rule next to the code it protects — the person adding destroy reads it in the same file instead of having to know that a route file three directories away is what stops it being public.

Controller middleware runs inside the route's group and outside the action, so a group's session still wraps the controller's authorisation check.

Testing an action

Because an action is a function, you can call it:

#[tokio::test]
async fn health_is_ok() {
    assert_eq!(health().await, "ok");
}

For anything touching the container or middleware, drive the real kernel instead — see Testing.

This page is the framework’s docs/controllers.md at 7b8cb15. Found a mistake? Edit it on GitHub.

Type to search 2.0.0 docs.