Skip to main content
Kit provides a simple pattern for handling incoming request data with automatic validation. The #[request] attribute works with both JSON and form-urlencoded data, making it suitable for REST APIs and HTML forms alike.

Overview

Requests combine three concerns into a single, declarative struct:
  1. Data parsing - Automatically parse JSON or form-urlencoded data
  2. Validation - Validate fields using the validator crate
  3. Authorization - Optionally check if the request is authorized

The #[handler] Attribute

All controller methods in Kit use the #[handler] attribute. This enables automatic extraction and validation of request data:
use kit::{handler, json_response, Request, Response};

// Simple handler with Request
#[handler]
pub async fn index(req: Request) -> Response {
    json_response!({ "message": "Hello" })
}
When combined with validated request types, validation happens automatically:
use kit::{handler, json_response, Response};
use crate::requests::CreateUserRequest;

// Request handler - automatically validates incoming data
#[handler]
pub async fn store(form: CreateUserRequest) -> Response {
    // `form` is already validated - this code only runs if validation passes
    json_response!({
        "email": form.email,
        "name": form.name
    })
}

Defining a Request

The #[request] attribute automatically adds Deserialize and Validate derives:
use kit::request;

#[request]
pub struct CreateUserRequest {
    #[validate(email(message = "Please provide a valid email address"))]
    pub email: String,

    #[validate(length(min = 8, message = "Password must be at least 8 characters"))]
    pub password: String,

    #[validate(length(min = 1, max = 100, message = "Name is required"))]
    pub name: String,
}

Validation Rules

Kit uses the validator crate for validation. Here are common validation rules:

String Validations

#[request]
pub struct ExampleRequest {
    // Required (non-empty)
    #[validate(length(min = 1, message = "This field is required"))]
    pub name: String,

    // Email format
    #[validate(email(message = "Invalid email address"))]
    pub email: String,

    // URL format
    #[validate(url(message = "Invalid URL"))]
    pub website: String,

    // Length constraints
    #[validate(length(min = 8, max = 100))]
    pub password: String,

    // Regex pattern
    #[validate(regex(path = "PHONE_REGEX", message = "Invalid phone number"))]
    pub phone: String,
}

Numeric Validations

#[request]
pub struct ProductRequest {
    // Range validation
    #[validate(range(min = 0, max = 10000, message = "Price must be between 0 and 10000"))]
    pub price: f64,

    // Minimum value
    #[validate(range(min = 1))]
    pub quantity: i32,

    // Maximum value
    #[validate(range(max = 100))]
    pub discount_percent: i32,
}

Nested and Collection Validations

use serde::Deserialize;

#[derive(Deserialize, Validate)]
pub struct Address {
    #[validate(length(min = 1))]
    pub street: String,

    #[validate(length(min = 1))]
    pub city: String,
}

#[request]
pub struct OrderRequest {
    // Nested struct validation
    #[validate(nested)]
    pub shipping_address: Address,

    // Collection length
    #[validate(length(min = 1, message = "At least one item required"))]
    pub items: Vec<String>,
}

Common Validation Attributes

AttributeDescriptionExample
emailValid email format#[validate(email)]
urlValid URL format#[validate(url)]
lengthString/collection length#[validate(length(min = 1, max = 100))]
rangeNumeric range#[validate(range(min = 0, max = 100))]
regexRegex pattern match#[validate(regex(path = "PATTERN"))]
containsString contains substring#[validate(contains(pattern = "@"))]
does_not_containString doesn’t contain#[validate(does_not_contain(pattern = "admin"))]
nestedValidate nested struct#[validate(nested)]

Validation Error Response

When validation fails, Kit automatically returns a 422 response with Laravel/Inertia-compatible error format:
HTTP 422 Unprocessable Entity

{
    "message": "The given data was invalid.",
    "errors": {
        "email": ["Please provide a valid email address"],
        "password": ["Password must be at least 8 characters"]
    }
}
This format integrates seamlessly with Inertia.js form handling on the frontend.

Complete Example

Here’s a complete example of a user registration endpoint: Define the request:
// src/requests/create_user.rs
use kit::request;

#[request]
pub struct CreateUserRequest {
    #[validate(email(message = "Please provide a valid email address"))]
    pub email: String,

    #[validate(length(min = 8, message = "Password must be at least 8 characters"))]
    pub password: String,

    #[validate(length(min = 2, max = 50, message = "Name must be between 2 and 50 characters"))]
    pub name: String,
}
Create the controller:
// src/controllers/user.rs
use kit::{handler, json_response, Request, Response, ResponseExt};
use crate::requests::CreateUserRequest;

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

#[handler]
pub async fn store(form: CreateUserRequest) -> Response {
    // Validation passed - create the user
    // In a real app, you'd save to database here

    json_response!({
        "user": {
            "email": form.email,
            "name": form.name
        },
        "message": "User created successfully"
    })
    .status(201)
}
Register the routes:
// src/routes.rs
use kit::{get, post, routes};
use crate::controllers;

routes! {
    get("/users", controllers::user::index).name("users.index"),
    post("/users", controllers::user::store).name("users.store"),
}

Authorization

You can override the authorize method to add authorization checks:
use kit::request;

#[request]
pub struct DeleteUserRequest {
    pub user_id: i64,
}

impl DeleteUserRequest {
    fn authorize(req: &kit::Request) -> bool {
        // Check if user has admin role
        // Return false to reject with 403 Forbidden
        req.header("X-Admin-Token").is_some()
    }
}
If authorize returns false, the request is rejected with a 403 Forbidden response:
HTTP 403 Forbidden

{
    "message": "This action is unauthorized."
}

Request Content Types

Requests automatically detect and parse the content type:
  • application/json - Parsed as JSON
  • application/x-www-form-urlencoded - Parsed as form data
The parsing is handled automatically based on the Content-Type header.

Using Request with Validated Data

If you need access to both the validated data and the original request (for headers, params, etc.), you can still access request information in your controller:
use kit::{handler, json_response, Response, App};
use crate::requests::CreateUserRequest;
use crate::services::UserService;

#[handler]
pub async fn store(form: CreateUserRequest) -> Response {
    // Access services via dependency injection
    let user_service = App::resolve::<UserService>();

    // Use the validated form data
    let user = user_service.create_user(&form.email, &form.name);

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

File Organization

The standard structure for requests:
src/
├── requests/
│   ├── mod.rs                 # Re-exports all requests
│   ├── create_user.rs         # CreateUserRequest
│   ├── update_user.rs         # UpdateUserRequest
│   └── create_post.rs         # CreatePostRequest
├── controllers/
│   └── user.rs                # Uses CreateUserRequest
└── routes.rs
src/requests/mod.rs:
pub mod create_user;
pub mod update_user;

pub use create_user::CreateUserRequest;
pub use update_user::UpdateUserRequest;

End-to-End Type Safety with Inertia

Requests can also derive InertiaProps to generate TypeScript types, enabling end-to-end type safety from your Rust backend to your React frontend.

Generating TypeScript Types for Requests

Add InertiaProps derive alongside #[request]:
use kit::{request, InertiaProps};

#[request]
#[derive(InertiaProps)]
pub struct CreateTodoRequest {
    #[validate(length(min = 1, message = "Title is required"))]
    pub title: String,

    #[validate(length(max = 500))]
    pub description: Option<String>,
}
Run type generation:
kit generate-types
This generates TypeScript types in frontend/src/types/inertia-props.ts:
export interface CreateTodoRequest {
  title: string
  description: string | null
}

Type-Safe Forms with Inertia

Use Inertia’s <Form> component for the cleanest form handling:
import { Form, usePage } from '@inertiajs/react'

export default function CreateTodo() {
  const { errors } = usePage().props

  return (
    <Form action="/todos" method="post">
      <input
        type="text"
        name="title"
        placeholder="Todo title"
      />
      {errors?.title && <span className="error">{errors.title}</span>}

      <textarea
        name="description"
        placeholder="Description (optional)"
      />

      <button type="submit">Create Todo</button>
    </Form>
  )
}
For more control, combine <Form> with the useForm hook and your generated types:
import { Form, useForm } from '@inertiajs/react'
import type { CreateTodoRequest } from '../types/inertia-props'

export default function CreateTodo() {
  const { data, setData, errors, processing } = useForm<CreateTodoRequest>({
    title: '',
    description: null,
  })

  return (
    <Form action="/todos" method="post">
      {({ processing }) => (
        <>
          <input
            type="text"
            name="title"
            value={data.title}
            onChange={(e) => setData('title', e.target.value)}
            placeholder="Todo title"
          />
          {errors.title && <span className="error">{errors.title}</span>}

          <textarea
            name="description"
            value={data.description || ''}
            onChange={(e) => setData('description', e.target.value || null)}
            placeholder="Description (optional)"
          />

          <button type="submit" disabled={processing}>
            Create Todo
          </button>
        </>
      )}
    </Form>
  )
}

Benefits of End-to-End Type Safety

  1. Compile-time checks: TypeScript catches field name typos and type mismatches
  2. IDE autocomplete: Full IntelliSense for form fields in your editor
  3. Validation alignment: Your TypeScript types match your Rust validation rules
  4. Refactoring safety: Rename a field in Rust, TypeScript errors show where to update

Workflow

  1. Define request with validation in Rust
  2. Add #[derive(InertiaProps)] to the struct
  3. Run kit generate-types to generate TypeScript
  4. Use the generated type with useForm<RequestType>
  5. Get full type safety and validation error handling
For more information on TypeScript type generation, see TypeScript Types.

Summary

FeatureDescription
Define request#[request] attribute
Handler attribute#[handler] on all controller methods
ValidationUse #[validate(...)] attributes
Error formatLaravel/Inertia-compatible 422 JSON
AuthorizationOverride authorize() method
Auto content-typeDetects JSON vs form-urlencoded
TypeScript typesAdd #[derive(InertiaProps)] for type generation
Type-safe formsUse generated types with useForm<T>