Skip to main content
Kit provides a Laravel/Eloquent-inspired fluent API for working with database models, making common CRUD operations simple and intuitive.

Entity Structure

Kit uses SeaORM entities under the hood. When you run kit db:sync, two files are generated for each table:
  1. Auto-generated entity (src/models/entities/{table}.rs) - SeaORM entity definition, regenerated on sync
  2. User model (src/models/{table}.rs) - Your custom code with the fluent API, never overwritten
// src/models/todos.rs (generated with fluent API)
pub use super::entities::todos::*;

use kit::database::{ModelMut, QueryBuilder};
use sea_orm::{entity::prelude::*, Set};

/// Type alias for convenient access
pub type Todo = Model;

impl Model {
    pub fn query() -> QueryBuilder<Entity> { ... }
    pub fn create() -> TodoBuilder { ... }
    pub fn set_title(mut self, value: impl Into<String>) -> Self { ... }
    pub async fn update(self) -> Result<Self, FrameworkError> { ... }
    pub async fn delete(self) -> Result<u64, FrameworkError> { ... }
}

pub struct TodoBuilder { ... }

Generating Models

Use the db:sync command to generate models from your database:
# Run migrations and generate models
kit db:sync

# Skip migrations, just regenerate entities
kit db:sync --skip-migrations

# Regenerate all model files (including user models with fluent API)
kit db:sync --regenerate-models
The --regenerate-models flag will overwrite your custom model files. Use with caution if you’ve added custom methods.

Eloquent-like Fluent API

The generated models provide a fluent, chainable API similar to Laravel’s Eloquent.

Querying Records

Use Model::query() to start a fluent query builder:
use crate::models::todos::{Todo, Column};

// Get all records
let todos = Todo::query().all().await?;

// Filter with conditions
let active_todos = Todo::query()
    .filter(Column::Completed.eq(false))
    .all()
    .await?;

// Chain multiple filters
let urgent = Todo::query()
    .filter(Column::Priority.eq("high"))
    .filter(Column::Completed.eq(false))
    .all()
    .await?;

// Get first record
let first = Todo::query().first().await?; // Option<Model>

// Get first or fail
let todo = Todo::query()
    .filter(Column::Id.eq(1))
    .first_or_fail()
    .await?;

// Count records
let count = Todo::query()
    .filter(Column::Completed.eq(true))
    .count()
    .await?;

// Check existence
let has_incomplete = Todo::query()
    .filter(Column::Completed.eq(false))
    .exists()
    .await?;

// Ordering and pagination
let recent = Todo::query()
    .order_by_desc(Column::CreatedAt)
    .limit(10)
    .offset(0)
    .all()
    .await?;

Creating Records

Use Model::create() to get a fluent builder for creating new records:
use crate::models::todos::Todo;

// Create with fluent setters
let todo = Todo::create()
    .set_title("Learn Kit")
    .set_description("Build something awesome")
    .insert()
    .await?;

println!("Created todo with ID: {}", todo.id);

Updating Records

Chain setters on an existing model and call update():
use crate::models::todos::{Todo, Column};

// Find and update
let todo = Todo::query()
    .filter(Column::Id.eq(1))
    .first_or_fail()
    .await?;

let updated = todo
    .set_title("Updated title")
    .update()
    .await?;

Deleting Records

Call delete() on a model instance:
let todo = Todo::query()
    .filter(Column::Id.eq(1))
    .first_or_fail()
    .await?;

todo.delete().await?;

Complete Example

Here’s a complete CRUD controller using the fluent API:
// src/controllers/todos.rs
use kit::{handler, json_response, Request, Response};
use crate::models::todos::{Todo, Column};

#[handler]
pub async fn index(_req: Request) -> Response {
    match Todo::query().all().await {
        Ok(todos) => json_response!({ "todos": todos }),
        Err(e) => json_response!({ "error": e.to_string() }),
    }
}

#[handler]
pub async fn show(req: Request) -> Response {
    let id: i32 = req.param("id").unwrap().parse().unwrap();

    match Todo::query().filter(Column::Id.eq(id)).first_or_fail().await {
        Ok(todo) => json_response!({ "todo": todo }),
        Err(_) => json_response!({ "error": "Todo not found" }),
    }
}

#[handler]
pub async fn store(req: Request) -> Response {
    let title = "New Todo".to_string();

    match Todo::create()
        .set_title(title)
        .set_description("A new todo item")
        .insert()
        .await
    {
        Ok(todo) => json_response!({ "todo": todo }),
        Err(e) => json_response!({ "error": e.to_string() }),
    }
}

#[handler]
pub async fn update(req: Request) -> Response {
    let id: i32 = req.param("id").unwrap().parse().unwrap();

    let todo = Todo::query()
        .filter(Column::Id.eq(id))
        .first_or_fail()
        .await
        .unwrap();

    match todo.set_title("Updated title").update().await {
        Ok(updated) => json_response!({ "todo": updated }),
        Err(e) => json_response!({ "error": e.to_string() }),
    }
}

#[handler]
pub async fn destroy(req: Request) -> Response {
    let id: i32 = req.param("id").unwrap().parse().unwrap();

    let todo = Todo::query()
        .filter(Column::Id.eq(id))
        .first_or_fail()
        .await
        .unwrap();

    match todo.delete().await {
        Ok(_) => json_response!({ "deleted": true }),
        Err(e) => json_response!({ "error": e.to_string() }),
    }
}

Model and ModelMut Traits

The fluent API is built on top of two core traits that you can also use directly:

The Model Trait

Provides read-only operations on the Entity:
use kit::database::Model;
use crate::models::todos::Entity as Todos;

// These methods are available on Entity
let todos = Todos::all().await?;
let todo = Todos::find_by_pk(1).await?;
let todo = Todos::find_or_fail(1).await?;
let first = Todos::first().await?;
let count = Todos::count_all().await?;
let exists = Todos::exists_any().await?;
Implementing kit::database::Model also enables Route Model Binding, allowing you to use Model types directly as handler parameters for automatic database lookup.

The ModelMut Trait

Provides write operations:
use kit::database::ModelMut;
use crate::models::todos::{Entity as Todos, ActiveModel};
use sea_orm::ActiveValue::Set;

// Insert
let new_todo = ActiveModel {
    title: Set("Learn Kit".to_string()),
    ..Default::default()
};
let created = Todos::insert_one(new_todo).await?;

// Update
let mut active: ActiveModel = existing_todo.into();
active.title = Set("Updated".to_string());
let updated = Todos::update_one(active).await?;

// Delete
let deleted = Todos::delete_by_pk(1).await?;

// Save (insert or update)
let saved = Todos::save_one(active_model).await?;

Working with ActiveModel

For advanced use cases, you can work directly with SeaORM’s ActiveModel:
use sea_orm::ActiveValue::{Set, NotSet, Unchanged};

// Creating a new record
let new_todo = ActiveModel {
    id: NotSet,                        // Auto-increment
    title: Set("New task".to_string()),
    completed: Set(false),
    created_at: NotSet,                // Use database default
    updated_at: NotSet,
};

// Updating specific fields
let mut update_todo = ActiveModel {
    id: Unchanged(1),                  // Keep the same ID
    title: Set("Updated title".to_string()),
    completed: Unchanged(false),       // Don't change this field
    ..Default::default()
};

ActiveValue States

StateMeaningSQL Behavior
Set(value)Set to this valueIncluded in INSERT/UPDATE
NotSetNot specifiedExcluded (uses default)
Unchanged(value)Keep current valueExcluded from UPDATE

Model Relationships

Define relationships in your model file:
// src/models/posts.rs
impl Entity {
    pub fn belongs_to_user() -> RelationDef {
        Entity::belongs_to(super::users::Entity)
            .from(Column::UserId)
            .to(super::users::Column::Id)
            .into()
    }
}

impl Related<super::users::Entity> for Entity {
    fn to() -> RelationDef {
        Self::belongs_to_user()
    }
}

Loading Relationships

use sea_orm::EntityTrait;
use kit::database::DB;
use crate::models::{posts, users};

// Find post with its user
let post_with_user = posts::Entity::find_by_id(1)
    .find_also_related(users::Entity)
    .one(DB::connection()?.inner())
    .await?;

if let Some((post, Some(user))) = post_with_user {
    println!("Post: {}, Author: {}", post.title, user.name);
}

API Summary

Fluent API (on Model)

MethodDescription
Model::query()Start a fluent query builder
Model::create()Start a fluent builder for creating
model.set_field(value)Set a field (chainable)
model.update()Save changes to database
model.delete()Delete the record

QueryBuilder Methods

MethodDescription
.filter(condition)Add a filter condition
.order_by_asc(column)Order ascending
.order_by_desc(column)Order descending
.limit(n)Limit results
.offset(n)Skip results
.all()Execute and get all results
.first()Get first result (Option)
.first_or_fail()Get first or error
.count()Count matching records
.exists()Check if any match

Entity Trait Methods

TraitMethodDescription
Modelall()Get all records
Modelfind_by_pk(id)Find by primary key
Modelfind_or_fail(id)Find or error
Modelfirst()Get first record
Modelcount_all()Count all records
Modelexists_any()Check if any exist
ModelMutinsert_one(model)Insert a record
ModelMutupdate_one(model)Update a record
ModelMutsave_one(model)Insert or update
ModelMutdelete_by_pk(id)Delete by primary key