Skip to main content
Kit controllers are async functions that handle HTTP requests and return responses. Following Laravel’s conventions, controllers organize your application’s request handling logic into dedicated modules, making your codebase clean and maintainable.

Generating Controllers

The fastest way to create a new controller is using the Kit CLI:
kit make:controller User
This command will:
  1. Create src/controllers/user.rs with a controller stub
  2. Update src/controllers/mod.rs to export the new controller
# Creates user.rs in src/controllers/
kit make:controller User

# Creates product.rs in src/controllers/
kit make:controller Product

# Controller name is converted to snake_case for the file
kit make:controller OrderItem  # Creates order_item.rs

Controller Structure

Controllers are async functions decorated with #[handler] that take a Request and return a Response:
use kit::{handler, Request, Response, json_response};

#[handler]
pub async fn index(_req: Request) -> Response {
    json_response!({
        "message": "Hello from controller"
    })
}

The Handler Attribute

All controller methods use the #[handler] attribute. This enables:
  • Automatic extraction and validation of request data
  • Clean integration with FormRequests for POST data validation
  • Future DX improvements like dependency injection
#[handler]
pub async fn handler_name(request: Request) -> Response
  • #[handler]: Required attribute that enables automatic parameter extraction
  • async fn: Controllers are asynchronous, allowing non-blocking I/O operations
  • Request: Contains all information about the incoming HTTP request
  • Response: An alias for Result<HttpResponse, HttpResponse>, enabling the ? operator
For handling POST data with validation, see Requests.

Path Parameter Injection

The #[handler] macro supports automatic extraction of path parameters directly as function arguments. This eliminates the need to manually call req.param().

Primitive Path Parameters

Declare path parameters as function arguments with primitive types:
use kit::{handler, json_response, Response};

// Route: get!("/users/{id}", controllers::user::show)
#[handler]
pub async fn show(id: i32) -> Response {
    json_response!({
        "user_id": id
    })
}

// Route: get!("/posts/{slug}", controllers::post::show)
#[handler]
pub async fn show_by_slug(slug: String) -> Response {
    json_response!({
        "slug": slug
    })
}

Multiple Path Parameters

Extract multiple parameters in a single handler:
// Route: get!("/posts/{post_id}/comments/{comment_id}", handler)
#[handler]
pub async fn show_comment(post_id: i32, comment_id: i32) -> Response {
    json_response!({
        "post_id": post_id,
        "comment_id": comment_id
    })
}

Supported Types

TypeDescription
i32, i64Signed integers
u32, u64, usizeUnsigned integers
StringString values
The parameter name in your function must match the route parameter name (e.g., {id} requires id: i32).

Route Model Binding

Route model binding automatically resolves database models from route parameters. If the model is not found, Kit automatically returns a 404 response.

Setting Up Route Binding

First, enable route binding for your model using the route_binding! macro:
// src/models/user.rs
use sea_orm::entity::prelude::*;
use kit::{route_binding, Model, ModelMut};

#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "users")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub name: String,
    pub email: String,
}

impl kit::Model for Entity {}
impl kit::ModelMut for Entity {}

// Enable route model binding - "user" matches {user} in routes
route_binding!(Entity, Model, "user");

Using Route Model Binding in Handlers

Now you can directly accept the model as a handler parameter:
use kit::{handler, json_response, Response};
use crate::models::user;

// Route: get!("/users/{user}", controllers::user::show)
#[handler]
pub async fn show(user: user::Model) -> Response {
    // user is automatically fetched from the database
    // Returns 404 if not found
    json_response!({
        "id": user.id,
        "name": user.name,
        "email": user.email
    })
}

Combining Models with Form Requests

Mix route model binding with form requests for update operations:
use kit::{handler, json_response, Response};
use crate::models::user;
use crate::requests::UpdateUserRequest;

// Route: put!("/users/{user}", controllers::user::update)
#[handler]
pub async fn update(user: user::Model, form: UpdateUserRequest) -> Response {
    // user: auto-fetched from database (404 if not found)
    // form: auto-validated from request body

    json_response!({
        "updated_user_id": user.id,
        "new_name": form.name
    })
}

Error Responses

ErrorStatus CodeDescription
Model not found404The requested resource doesn’t exist
Invalid parameter400Parameter couldn’t be parsed (e.g., “abc” as i32)
Missing parameter400Required route parameter is missing

Flexibility

You have full control over how you access route parameters:
// Option 1: Request passthrough (manual extraction)
#[handler]
pub async fn show(req: Request) -> Response {
    let id = req.param("id")?;
    // Manual database query...
}

// Option 2: Primitive path params (no model binding)
#[handler]
pub async fn show(id: i32) -> Response {
    // Just the ID, no automatic model fetch
}

// Option 3: Route model binding (auto-fetch with 404)
#[handler]
pub async fn show(user: user::Model) -> Response {
    // Model automatically fetched from database
}

// Option 4: Mix primitives and models
#[handler]
pub async fn nested(category_id: i32, product: product::Model) -> Response {
    // category_id: just extracted as i32
    // product: auto-fetched from database
}

The Request Object

The Request struct provides Laravel-like access to request data:

Getting Route Parameters

Access dynamic URL segments defined in your routes:
use kit::{Request, Response, json_response};

// Route: get!("/users/{id}", controllers::user::show)
pub async fn show(req: Request) -> Response {
    // Using ? operator - returns 400 error if param missing
    let id = req.param("id")?;

    json_response!({
        "user_id": id
    })
}
For routes with multiple parameters:
// Route: get!("/posts/{post_id}/comments/{comment_id}", handler)
pub async fn show_comment(req: Request) -> Response {
    let post_id = req.param("post_id")?;
    let comment_id = req.param("comment_id")?;

    json_response!({
        "post_id": post_id,
        "comment_id": comment_id
    })
}

Getting Headers

Access HTTP headers from the request:
pub async fn index(req: Request) -> Response {
    // Get a specific header (returns Option<&str>)
    let auth = req.header("Authorization");
    let content_type = req.header("Content-Type");

    if let Some(token) = auth {
        // Process authenticated request
    }

    json_response!({"status": "ok"})
}

Request Methods

MethodReturn TypeDescription
method()&MethodHTTP method (GET, POST, etc.)
path()&strRequest path (e.g., /users/123)
param(name)Result<&str, ParamError>Get a route parameter
params()&HashMap<String, String>Get all route parameters
header(name)Option<&str>Get a header value
is_inertia()boolCheck if Inertia.js request
inertia_version()Option<&str>Get Inertia version

Creating Responses

Controllers return responses using helper methods and macros:

JSON Responses

use kit::{json_response, Request, Response};

pub async fn index(_req: Request) -> Response {
    json_response!({
        "users": [
            {"id": 1, "name": "John"},
            {"id": 2, "name": "Jane"}
        ]
    })
}

Text Responses

use kit::{text_response, Request, Response};

pub async fn health(_req: Request) -> Response {
    text_response!("OK")
}

Setting Status Codes

use kit::{json_response, Request, Response, ResponseExt};

pub async fn store(_req: Request) -> Response {
    // Create user...

    json_response!({"id": 1, "created": true})
        .status(201)
}
For more response options, see the Responses documentation.

RESTful Controllers

Kit encourages organizing controllers following REST conventions:
// src/controllers/user.rs
use kit::{json_response, redirect, Request, Response, ResponseExt};

/// GET /users - List all users
pub async fn index(_req: Request) -> Response {
    json_response!({
        "users": [
            {"id": 1, "name": "John"},
            {"id": 2, "name": "Jane"}
        ]
    })
}

/// GET /users/{id} - Show a specific user
pub async fn show(req: Request) -> Response {
    let id = req.param("id")?;

    json_response!({
        "id": id,
        "name": format!("User {}", id)
    })
}

/// POST /users - Create a new user
pub async fn store(_req: Request) -> Response {
    // Create user logic...

    json_response!({"id": 1, "created": true})
        .status(201)
}

/// PUT /users/{id} - Update a user
pub async fn update(req: Request) -> Response {
    let id = req.param("id")?;

    // Update user logic...

    json_response!({
        "id": id,
        "updated": true
    })
}

/// DELETE /users/{id} - Delete a user
pub async fn destroy(req: Request) -> Response {
    let _id = req.param("id")?;

    // Delete user logic...

    redirect!("users.index").into()
}
Register these in your routes:
// src/routes.rs
use kit::{get, post, put, delete, routes};
use crate::controllers;

routes! {
    get!("/users", controllers::user::index).name("users.index"),
    get!("/users/{id}", controllers::user::show).name("users.show"),
    post!("/users", controllers::user::store).name("users.store"),
    put!("/users/{id}", controllers::user::update).name("users.update"),
    delete!("/users/{id}", controllers::user::destroy).name("users.destroy"),
}

Error Handling in Controllers

Use the ? operator for clean error propagation:
use kit::{AppError, Request, Response, json_response};

pub async fn show(req: Request) -> Response {
    // Returns 400 if param is missing
    let id = req.param("id")?;

    // Simulate database lookup
    let user = find_user(id).await?;

    json_response!({
        "user": user
    })
}

async fn find_user(id: &str) -> Result<User, AppError> {
    // Database query...
    if id == "999" {
        return Err(AppError::not_found("User not found"));
    }
    Ok(User { id: id.to_string(), name: "John".to_string() })
}
For more error handling options, see the Responses documentation.

Dependency Injection

Use App::resolve() to inject dependencies from the container:
use kit::{App, Request, Response, json_response};
use crate::actions::UserService;

pub async fn index(_req: Request) -> Response {
    // Resolve a service from the container
    let user_service = App::resolve::<UserService>();

    let users = user_service.list_all();

    json_response!({
        "users": users
    })
}

File Organization

The standard file structure for controllers:
src/
├── controllers/
│   ├── mod.rs          # Re-export all controllers
│   ├── home.rs         # Home controller
│   ├── user.rs         # User controller
│   ├── product.rs      # Product controller
│   └── api/            # Nested API controllers
│       ├── mod.rs
│       └── user.rs     # API user controller
├── routes.rs           # Route definitions
└── main.rs
src/controllers/mod.rs:
pub mod home;
pub mod user;
pub mod product;
pub mod api;
src/controllers/user.rs:
use kit::{json_response, Request, Response};

pub async fn index(_req: Request) -> Response {
    json_response!({"controller": "user"})
}

pub async fn show(req: Request) -> Response {
    let id = req.param("id")?;
    json_response!({"id": id})
}

Practical Examples

API Controller with Validation

use kit::{AppError, Request, Response, json_response, ResponseExt};

pub async fn store(req: Request) -> Response {
    // Get required header
    let content_type = req.header("Content-Type")
        .ok_or_else(|| AppError::bad_request("Content-Type header required"))?;

    if !content_type.contains("application/json") {
        return Err(AppError::bad_request("Content-Type must be application/json").into());
    }

    // Process the request...

    json_response!({"created": true})
        .status(201)
}

Controller with Redirects

use kit::{redirect, route, Request, Response};

pub async fn store(_req: Request) -> Response {
    // Create resource...

    // Redirect to named route
    redirect!("users.index").into()
}

pub async fn update(req: Request) -> Response {
    let id = req.param("id")?;

    // Update resource...

    // Redirect with route parameter
    redirect!("users.show")
        .with("id", id)
        .into()
}

pub async fn search(_req: Request) -> Response {
    // Redirect with query parameters
    redirect!("users.index")
        .query("page", "1")
        .query("sort", "name")
        .into()
}

Summary

FeatureUsage
Generate controllerkit make:controller Name
Handler attribute#[handler] on all controller methods
Handler signaturepub async fn name(req: Request) -> Response
Path param handlerpub async fn name(id: i32) -> Response
Route model bindingpub async fn name(user: user::Model) -> Response
FormRequest handlerpub async fn name(form: FormRequest) -> Response
Mixed paramspub async fn name(user: user::Model, form: UpdateRequest) -> Response
Enable route bindingroute_binding!(Entity, Model, "param_name")
Get route paramreq.param("id")?
Get all paramsreq.params()
Get headerreq.header("Authorization")
Get HTTP methodreq.method()
Get pathreq.path()
JSON responsejson_response!({...})
Text responsetext_response!("...")
Set status.status(201)
Redirectredirect!("route.name").into()