# Building Your First Rustelo Application
RUSTELO
RUSTELO
Welcome to building your first[Rustelo](/)application! This guide will walk you through creating a complete web application from scratch, covering both frontend and backend development with Rustelo's powerful features. ## 🎯 What You'll Build By the end of this guide, you'll have created a **Task Management Application** with: - 📝 **Frontend**: Interactive task list with Leptos components - 🔧 **Backend**: RESTful API with Axum server - 🗄️ **Database**: Data persistence with SQLx - 🔐 **Authentication**: User login and registration - 🎨 **Styling**: Modern UI with Tailwind CSS - 📱 **Responsive**: Mobile-friendly design ## 🚀 Prerequisites Before starting, ensure you have: - ✅ **Rustelo installed** - Follow the [Installation Guide](./installation.md) - ✅ **Basic Rust knowledge** - Variables, structs, functions - ✅ **Web development basics** - HTML, CSS, HTTP concepts - ✅ **Development environment** - Your favorite code editor ## 📋 Step 1: Project Setup ### Create Your Project ```bash # Clone the[Rustelo](/) template git clone https://github.com/yourusername/rustelo.git my-task-app cd my-task-app # Run the installer ./scripts/install.sh ``` ### Configure Your Application ```bash # Set up environment variables cat > .env << EOF # Database Configuration DATABASE_URL=sqlite//:tasks.db # Security Configuration SESSION_SECRET=your-development-session-secret-here JWT_SECRET=your-development-jwt-secret-here # Application Configuration RUSTELO_ENV=development SERVER_HOST=127.0.0.1 SERVER_PORT=3030 LOG_LEVEL=debug # Features Configuration ENABLE_AUTH=true ENABLE_CONTENT_DB=true ENABLE_EMAIL=false ENABLE_TLS=false EOF ``` ### Build Your Configuration ```bash # Build development configuration ./config/scripts/build-config.sh dev # Verify setup just verify-setup ``` ## 🏗️ Step 2: Database Schema ### Create Database Migration ```bash # Create migration for tasks table sqlx migrate add create_tasks_table ``` Edit the migration file in `migrations/`: ```sql -- migrations/001_create_tasks_table.sql CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, completed BOOLEAN NOT NULL DEFAULT FALSE, priority INTEGER NOT NULL DEFAULT 1, due_date TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, user_id INTEGER, FOREIGN KEY (user_id) REFERENCES users(id) ); -- Index for faster queries CREATE INDEX IF NOT EXISTS idx_tasks_user_id ON tasks(user_id); CREATE INDEX IF NOT EXISTS idx_tasks_completed ON tasks(completed); ``` ### Run Migration ```bash # Apply database migrations just db-migrate # Verify database structure just db-status ``` ## 🔧 Step 3: Shared Types Create shared data structures in `shared/src/types.rs`: ```rust // shared/src/types.rs use serde::{Deserialize, Serialize}; use chrono::{DateTime, Utc}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Task { pub id: Option, pub title: String, pub description: Option, pub completed: bool, pub priority: TaskPriority, pub due_date: Option>, pub created_at: DateTime, pub updated_at: DateTime, pub user_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum TaskPriority { Low = 1, Medium = 2, High = 3, Urgent = 4, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateTaskRequest { pub title: String, pub description: Option, pub priority: TaskPriority, pub due_date: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdateTaskRequest { pub title: Option, pub description: Option, pub completed: Option, pub priority: Option, pub due_date: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TasksResponse { pub tasks: Vec, pub total: usize, pub page: usize, pub per_page: usize, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApiResponse { pub success: bool, pub data: Option, pub message: Option, } ``` Update `shared/src/lib.rs`: ```rust // shared/src/lib.rs pub mod types; pub mod utils; pub use types::*; ``` ## 🖥️ Step 4: Backend API ### Create Task Repository Create `server/src/repositories/task_repository.rs`: ```rust // server/src/repositories/task_repository.rs use sqlx::{SqlitePool, Row}; use chrono::{DateTime, Utc}; use shared::{Task, CreateTaskRequest, UpdateTaskRequest, TaskPriority}; use anyhow::Result; pub struct TaskRepository { pool: SqlitePool, } impl TaskRepository { pub fn new(pool: SqlitePool) -> Self { Self { pool } } pub async fn create_task(&self, request: CreateTaskRequest, user_id: i32) -> Result { let now = Utc::now(); let row = sqlx::query!( r#" INSERT INTO tasks (title, description, priority, due_date, created_at, updated_at, user_id) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id, title, description, completed, priority, due_date, created_at, updated_at, user_id "#, request.title, request.description, request.priority as i32, request.due_date.map(|d| d.to_rfc3339()), now.to_rfc3339(), now.to_rfc3339(), user_id ) .fetch_one(&self.pool) .await?; Ok(Task { id: Some(row.id), title: row.title, description: row.description, completed: row.completed, priority: match row.priority { 1 => TaskPriority::Low, 2 => TaskPriority::Medium, 3 => TaskPriority::High, 4 => TaskPriority::Urgent, _ => TaskPriority::Medium, }, due_date: row.due_date.and_then(|d| DateTime::parse_from_rfc3339(&d).ok()) .map(|d| d.with_timezone(&Utc)), created_at: DateTime::parse_from_rfc3339(&row.created_at).unwrap().with_timezone(&Utc), updated_at: DateTime::parse_from_rfc3339(&row.updated_at).unwrap().with_timezone(&Utc), user_id: Some(row.user_id), }) } pub async fn get_tasks_by_user(&self, user_id: i32, page: usize, per_page: usize) -> Result> { let offset = (page - 1) * per_page; let rows = sqlx::query!( r#" SELECT id, title, description, completed, priority, due_date, created_at, updated_at, user_id FROM tasks WHERE user_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ? "#, user_id, per_page as i32, offset as i32 ) .fetch_all(&self.pool) .await?; let tasks = rows.into_iter().map(|row| Task { id: Some(row.id), title: row.title, description: row.description, completed: row.completed, priority: match row.priority { 1 => TaskPriority::Low, 2 => TaskPriority::Medium, 3 => TaskPriority::High, 4 => TaskPriority::Urgent, _ => TaskPriority::Medium, }, due_date: row.due_date.and_then(|d| DateTime::parse_from_rfc3339(&d).ok()) .map(|d| d.with_timezone(&Utc)), created_at: DateTime::parse_from_rfc3339(&row.created_at).unwrap().with_timezone(&Utc), updated_at: DateTime::parse_from_rfc3339(&row.updated_at).unwrap().with_timezone(&Utc), user_id: Some(row.user_id), }).collect(); Ok(tasks) } pub async fn update_task(&self, task_id: i32, request: UpdateTaskRequest, user_id: i32) -> Result { // First check if task exists and belongs to user let existing = sqlx::query!( "SELECT id FROM tasks WHERE id = ? AND user_id = ?", task_id, user_id ) .fetch_optional(&self.pool) .await?; if existing.is_none() { return Err(anyhow::anyhow!("Task not found or access denied")); } let now = Utc::now(); // Build dynamic update query let mut update_fields = Vec::new(); let mut params: Vec<&dyn sqlx::Encode<'_, sqlx::Sqlite>> = Vec::new(); if let Some(title) = &request.title { update_fields.push("title = ?"); params.push(title); } if let Some(description) = &request.description { update_fields.push("description = ?"); params.push(description); } if let Some(completed) = &request.completed { update_fields.push("completed = ?"); params.push(completed); } if let Some(priority) = &request.priority { update_fields.push("priority = ?"); let priority_val = *priority as i32; params.push(&priority_val); } if let Some(due_date) = &request.due_date { update_fields.push("due_date = ?"); let due_date_str = due_date.to_rfc3339(); params.push(&due_date_str); } if update_fields.is_empty() { return Err(anyhow::anyhow!("No fields to update")); } update_fields.push("updated_at = ?"); let now_str = now.to_rfc3339(); params.push(&now_str); let query = format!( "UPDATE tasks SET {} WHERE id = ? AND user_id = ?", update_fields.join(", ") ); // Execute update sqlx::query(&query) .bind(task_id) .bind(user_id) .execute(&self.pool) .await?; // Return updated task let row = sqlx::query!( r#" SELECT id, title, description, completed, priority, due_date, created_at, updated_at, user_id FROM tasks WHERE id = ? "#, task_id ) .fetch_one(&self.pool) .await?; Ok(Task { id: Some(row.id), title: row.title, description: row.description, completed: row.completed, priority: match row.priority { 1 => TaskPriority::Low, 2 => TaskPriority::Medium, 3 => TaskPriority::High, 4 => TaskPriority::Urgent, _ => TaskPriority::Medium, }, due_date: row.due_date.and_then(|d| DateTime::parse_from_rfc3339(&d).ok()) .map(|d| d.with_timezone(&Utc)), created_at: DateTime::parse_from_rfc3339(&row.created_at).unwrap().with_timezone(&Utc), updated_at: DateTime::parse_from_rfc3339(&row.updated_at).unwrap().with_timezone(&Utc), user_id: Some(row.user_id), }) } pub async fn delete_task(&self, task_id: i32, user_id: i32) -> Result { let result = sqlx::query!( "DELETE FROM tasks WHERE id = ? AND user_id = ?", task_id, user_id ) .execute(&self.pool) .await?; Ok(result.rows_affected() > 0) } pub async fn count_tasks_by_user(&self, user_id: i32) -> Result { let row = sqlx::query!( "SELECT COUNT(*) as count FROM tasks WHERE user_id = ?", user_id ) .fetch_one(&self.pool) .await?; Ok(row.count as usize) } } ``` ### Create API Handlers Create `server/src/handlers/task_handlers.rs`: ```rust // server/src/handlers/task_handlers.rs use axum::{ extract::{Path, Query, State}, response::Json, http::StatusCode, }; use serde::{Deserialize, Serialize}; use shared::{CreateTaskRequest, UpdateTaskRequest, TasksResponse, ApiResponse}; use crate::{ repositories::task_repository::TaskRepository, auth::AuthUser, AppState, }; #[derive(Debug, Deserialize)] pub struct TaskQuery { pub page: Option, pub per_page: Option, } pub async fn create_task( State(state): State, AuthUser(user_id): AuthUser, Json(request): Json, ) -> Result>, StatusCode> { let task_repo = TaskRepository::new(state.db_pool.clone()); match task_repo.create_task(request, user_id).await { Ok(task) => Ok(Json(ApiResponse { success: true, data: Some(task), message: Some("Task created successfully".to_string()), })), Err(e) => { tracing::error!("Failed to create task: {}", e); Err(StatusCode::INTERNAL_SERVER_ERROR) } } } pub async fn get_tasks( State(state): State, AuthUser(user_id): AuthUser, Query(params): Query, ) -> Result>, StatusCode> { let task_repo = TaskRepository::new(state.db_pool.clone()); let page = params.page.unwrap_or(1); let per_page = params.per_page.unwrap_or(10).min(100); // Limit to 100 per page match task_repo.get_tasks_by_user(user_id, page, per_page).await { Ok(tasks) => { let total = task_repo.count_tasks_by_user(user_id).await.unwrap_or(0); let response = TasksResponse { tasks, total, page, per_page, }; Ok(Json(ApiResponse { success: true, data: Some(response), message: None, })) } Err(e) => { tracing::error!("Failed to get tasks: {}", e); Err(StatusCode::INTERNAL_SERVER_ERROR) } } } pub async fn update_task( State(state): State, AuthUser(user_id): AuthUser, Path(task_id): Path, Json(request): Json, ) -> Result>, StatusCode> { let task_repo = TaskRepository::new(state.db_pool.clone()); match task_repo.update_task(task_id, request, user_id).await { Ok(task) => Ok(Json(ApiResponse { success: true, data: Some(task), message: Some("Task updated successfully".to_string()), })), Err(e) => { tracing::error!("Failed to update task: {}", e); if e.to_string().contains("not found") { Err(StatusCode::NOT_FOUND) } else { Err(StatusCode::INTERNAL_SERVER_ERROR) } } } } pub async fn delete_task( State(state): State, AuthUser(user_id): AuthUser, Path(task_id): Path, ) -> Result>, StatusCode> { let task_repo = TaskRepository::new(state.db_pool.clone()); match task_repo.delete_task(task_id, user_id).await { Ok(true) => Ok(Json(ApiResponse { success: true, data: None, message: Some("Task deleted successfully".to_string()), })), Ok(false) => Err(StatusCode::NOT_FOUND), Err(e) => { tracing::error!("Failed to delete task: {}", e); Err(StatusCode::INTERNAL_SERVER_ERROR) } } } ``` ### Update API Routes Update `server/src/main.rs` to include task routes: ```rust // Add to server/src/main.rs use axum::{ routing::{get, post, put, delete}, Router, }; mod handlers; mod repositories; use handlers::task_handlers::*; // In your app router setup: pub fn api_routes() -> Router { Router::new() .route("/api/tasks", post(create_task)) .route("/api/tasks", get(get_tasks)) .route("/api/tasks/:id", put(update_task)) .route("/api/tasks/:id", delete(delete_task)) // ... other routes } ``` ## 🎨 Step 5: Frontend Components ### Create Task Components Create `client/src/components/tasks/task_item.rs`: ```rust // client/src/components/tasks/task_item.rs use leptos::prelude::*; use shared::{Task, TaskPriority}; use chrono::{DateTime, Utc}; #[component] pub fn TaskItem( task: Task, #[prop(into)] on_toggle: Callback, #[prop(into)] on_delete: Callback, #[prop(into)] on_edit: Callback, ) -> impl IntoView { let task_id = task.id.unwrap_or(0); let is_completed = task.completed; let is_overdue = task.due_date .map(|due| due < Utc::now() && !task.completed) .unwrap_or(false); let priority_class = match task.priority { TaskPriority::Low => "border-l-green-400", TaskPriority::Medium => "border-l-yellow-400", TaskPriority::High => "border-l-orange-400", TaskPriority::Urgent => "border-l-red-400", }; let priority_text = match task.priority { TaskPriority::Low => "Low", TaskPriority::Medium => "Medium", TaskPriority::High => "High", TaskPriority::Urgent => "Urgent", }; view! {

{task.title.clone()}

{task.description.clone().map(|desc| view! {

{desc}

})}
"bg-green-100 text-green-800", TaskPriority::Medium => "bg-yellow-100 text-yellow-800", TaskPriority::High => "bg-orange-100 text-orange-800", TaskPriority::Urgent => "bg-red-100 text-red-800", } )> {priority_text} {task.due_date.map(|due| { let due_str = due.format("%Y-%m-%d %H:%M").to_string(); view! { "Due: " {due_str} } })}
} } ``` ### Create Task List Component Create `client/src/components/tasks/task_list.rs`: ```rust // client/src/components/tasks/task_list.rs use leptos::prelude::*; use shared::{Task, TasksResponse, ApiResponse}; use crate::components::tasks::task_item::TaskItem; use crate::api::tasks::*; #[component] pub fn TaskList() -> impl IntoView { let (tasks, set_tasks) = signal(Vec::::new()); let (loading, set_loading) = signal(false); let (error, set_error) = signal(None::); let (current_page, set_current_page) = signal(1); let (total_pages, set_total_pages) = signal(1); // Load tasks on component mount Effect::new(move |_| { spawn_local(async move { set_loading(true); set_error(None); match fetch_tasks(current_page.get_untracked(), 10).await { Ok(ApiResponse { success: true, data: Some(response), .. }) => { set_tasks(response.tasks); set_total_pages((response.total + response.per_page - 1) / response.per_page); } Ok(ApiResponse { message: Some(msg), .. }) => { set_error(Some(msg)); } Err(e) => { set_error(Some(format!("Failed to load tasks: {}", e))); } } set_loading(false); }); }); let handle_toggle = Callback::new(move |task_id: i32| { spawn_local(async move { // Find the task and toggle its completion status let current_tasks = tasks.get_untracked(); if let Some(task) = current_tasks.iter().find(|t| t.id == Some(task_id)) { let update_request = shared::UpdateTaskRequest { completed: Some(!task.completed), title: None, description: None, priority: None, due_date: None, }; match update_task(task_id, update_request).await { Ok(ApiResponse { success: true, data: Some(updated_task), .. }) => { let mut new_tasks = current_tasks; if let Some(index) = new_tasks.iter().position(|t| t.id == Some(task_id)) { new_tasks[index] = updated_task; set_tasks(new_tasks); } } Err(e) => { set_error(Some(format!("Failed to update task: {}", e))); } } } }); }); let handle_delete = Callback::new(move |task_id: i32| { spawn_local(async move { match delete_task(task_id).await { Ok(ApiResponse { success: true, .. }) => { let current_tasks = tasks.get_untracked(); let new_tasks: Vec = current_tasks .into_iter() .filter(|t| t.id != Some(task_id)) .collect(); set_tasks(new_tasks); } Err(e) => { set_error(Some(format!("Failed to delete task: {}", e))); } } }); }); let handle_edit = Callback::new(move |task: Task| { // For now, just log - you can implement an edit modal here logging::log!("Edit task: {:?}", task); }); view! {

"My Tasks"

"Manage your tasks efficiently"

{move || { if loading.get() { view! {

"Loading tasks..."

} } else if let Some(error_msg) = error.get() { view! {

"Error"

{error_msg}

} } else { let task_list = tasks.get(); if task_list.is_empty() { view! {