Skip to content

Controller basics

Rwf comes with multiple pre-built controllers that can be used out of the box, for example, to handle WebSocket connections, REST-style interactions, or serving static files. For everything else, the Controller trait can be implemented to handle any kind of HTTP requests.

What's a controller?

The controller is the C in MVC: it handles user interactions with the web app and performs actions on their behalf. A controller takes care of user inputs, like forms, and all other HTTP requests to the app.

Writing a controller

A controller is a plain Rust struct that implements the Controller trait. As an example, let's write a controller that returns the current time in UTC.

Import types

use rwf::prelude::*;

The prelude module contains most of the types and traits necessary to work with Rwf. Including it will save you time and effort when writing code, but it's not required.

Define the struct

#[derive(Default)]
struct CurrentTime;

This struct has no fields, but you can add any internal state you want to keep track of in there. The Default trait is derived automatically to provide a convenient way to instantiate it.

Implement the Controller trait

#[async_trait]
impl Controller for CurrentTime {
    /// This function handles incoming HTTP requests.
    async fn handle(&self, request: &Request) -> Result<Response, Error> {
        let time = OffsetDateTime::now_utc();

        // This creates an HTTP "200 OK" response,
        // with "Content-Type: text/plain" header.
        let response = Response::new()
            .text(format!("The current time is: {:?}", time));

        Ok(response)
    }
}

The Controller trait is asynchronous. Support for async traits in Rust is still incomplete, so we use the async_trait library to make it easy to use. The trait itself has a few methods, most of which have reasonable defaults. The only method that needs to be written by hand is async fn handle().

handle

The handle method accepts a Request and must return a Response. The response can be any valid HTTP response, including 404 or even 500.

Errors

If an error occurs inside the async fn handle function, Rwf will return HTTP 500 automatically and display the error to the client.

Connecting controllers

Once you implement a controller, adding it to the app requires mapping it to a route. A route is a unique URL, starting at the root of the app. For example, /signup is a route that could map to the Signup controller, and allow your users to create accounts.

Adding controllers to the app happens at server startup. A server can be launched from an async task anywhere in the code, but typically is done so from the main function:

use rwf::prelude::*;
use rwf::http::{self, Server};

#[tokio::main]
async fn main() -> Result<(), http::Error> {
    Server::new(vec![
        // Map the `/time` route to the `CurrentTime` controller.
        route!("/time" => CurrentTime),
    ])
    .launch("0.0.0.0:8000")
    .await
}

Note

The route! macro is a shorthand for calling CurrentTime::default().route("/time"). We use it because it looks cool, but it's not required. You can instantiate your controller struct in any way you need, and call the Controller::route method when adding it to the server. Alternatively, you can implement the Default trait like we did in this example and use the macro.

Test with cURL

Once the server is up and running, you can test your endpoints with cURL (or with a regular browser, like Firefox):

curl localhost:8000/time -w '\n'
The current time is: 2024-10-17 0:23:34.6191103 +00:00:00

Split GET from POST

Controllers that implement the Controller trait don't make a distinction between HTTP request methods and handle all of them in one function. Most websites show pages via GET requests and accept form submissions via POST requests. To avoid writing boilerplate code in the handle method, Rwf has another type of controller, PageController which splits up the two methods into their own functions: async fn get and async fn post:

#[derive(Default, macros::PageController)]
struct Login;

impl PageController for Login {
    /// Handle GET request.
    async fn get(&self, request: &Request) -> Result<Response, Error> {
        /* show page, by rendering a template */
    }

    /// Handle POST request.
    async fn post(&self, request: &Request) -> Result<Response, Error> {
        let form = request.form_data();

        /* process form submission and redirect */
    }
}

Learn more

Read more about working with controllers, requests, and responses: