Step-by-Step Tutorial: Building a Fullstack Task Manager with Yaiko
Welcome to the official Yaiko Framework Tutorial! In this guide, you will build a production-ready, fullstack Task Manager application from scratch using Rust on the backend and jQuery on the frontend.
📋 What You Will Learn
- CLI Scaffolding: Create a new app with
yaiko init. - Environment & Diagnostics: Use
yaiko doctorto verify system setup. - Hot-Reloading Development: Use
yaiko devfor real-time development. - Controllers & Models: Generate code modules with
yaiko generate. - Database & Migrations: Work with SQLite/PostgreSQL migrations (
yaiko migrate). - Frontend UI: Serve HTML templates and handle interactivity with jQuery.
- Automated Testing: Write integration tests with
yaiko_core::TestClient. - Production Deployment: Compile optimized release binaries with
yaiko build --release.
Prerequisites
- Rust 1.70+:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - Git
Step 1: Install the Yaiko CLI
Clone the repository and install the CLI globally:
# Clone the repository
git clone https://github.com/sazalo101/yaiko.git
cd yaiko
# Install CLI globally
cargo install --path ./yaiko-cli --force
# Verify CLI installation & environment readiness
yaiko doctor
Step 2: Initialize Your Project
Create a new application named taskmanager with SQLite database support:
yaiko init taskmanager --database sqlite
cd taskmanager
Directory Structure Created:
taskmanager/
├── src/
│ ├── main.rs # Application entry point & router
│ ├── controllers/ # Route handlers
│ │ └── users.rs
│ ├── models/ # Data models & DB logic
│ └── middleware/ # Custom request middleware
├── public/
│ ├── css/main.css # Modern glassmorphism styles
│ └── js/
│ ├── core.js # Yaiko UI & Toast utilities
│ └── app.js # Frontend interactivity
├── templates/
│ └── index.html # Landing page HTML
├── migrations/ # SQL migration files
├── yaiko.toml # Framework configuration
├── .env # Environment variables
└── Cargo.toml # Rust dependencies
Step 3: Start the Hot-Reloading Development Server
Launch the development server:
yaiko dev
Output:
âš¡ Starting Yaiko dev server at http://127.0.0.1:3000...
[info] Watching src/, templates/, public/ for changes...
Open http://127.0.0.1:3000 in your browser. You will see the default Yaiko welcome page!
Step 4: Create a Database Migration
Stop yaiko dev (press Ctrl+C) and generate a migration for the tasks table:
yaiko migrate create tasks
This creates a file in migrations/YYYYMMDD_tasks.sql. Add the following SQL schema:
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO tasks (title, completed) VALUES ('Learn Yaiko Framework', 1);
INSERT INTO tasks (title, completed) VALUES ('Build Task Manager App', 0);
Run the database migration:
yaiko migrate run
Step 5: Generate Task Controller & Model
Use yaiko generate to scaffold the backend modules:
yaiko generate model task
yaiko generate controller tasks
1. Update src/models/task.rs:
use serde::{Deserialize, Serialize};
use sqlx::SqlitePool;
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct Task {
pub id: i64,
pub title: String,
pub completed: bool,
}
impl Task {
pub async fn all(pool: &SqlitePool) -> Result<Vec<Task>, sqlx::Error> {
sqlx::query_as::<_, Task>("SELECT id, title, completed FROM tasks ORDER BY id DESC")
.fetch_all(pool)
.await
}
pub async fn create(pool: &SqlitePool, title: &str) -> Result<Task, sqlx::Error> {
let id = sqlx::query("INSERT INTO tasks (title, completed) VALUES (?, FALSE)")
.bind(title)
.execute(pool)
.await?
.last_insert_rowid();
Ok(Task {
id,
title: title.to_string(),
completed: false,
})
}
}
2. Update src/controllers/tasks.rs:
use yaiko_core::{Request, Response, StatusCode, json, BoxError};
use sqlx::SqlitePool;
use crate::models::task::Task;
pub async fn list(req: Request, pool: SqlitePool) -> Result<Response, BoxError> {
let tasks = Task::all(&pool).await?;
Ok(Response::new().json(&json!({ "tasks": tasks }))?)
}
pub async fn create(mut req: Request, pool: SqlitePool) -> Result<Response, BoxError> {
let body = req.json().await?;
let title = body["title"].as_str().unwrap_or("").trim();
if title.is_empty() {
return Ok(Response::new()
.status(StatusCode::BAD_REQUEST)
.json(&json!({ "error": "Title cannot be empty" }))?);
}
let task = Task::create(&pool, title).await?;
Ok(Response::new()
.status(StatusCode::CREATED)
.json(&json!({ "task": task }))?)
}
Step 6: Connect Routes in src/main.rs
Open src/main.rs and update your router and database pool connection:
use yaiko_core::{App, Router, Server, Request, Response, Settings, BoxError, LoggingMiddleware};
use sqlx::sqlite::SqlitePoolOptions;
use std::net::SocketAddr;
mod controllers;
mod models;
#[tokio::main]
async fn main() -> Result<(), BoxError> {
dotenvy::dotenv().ok();
let settings = Settings::load()?;
yaiko_core::init_tracing();
let db_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite:./data.db?mode=rwc".into());
let pool = SqlitePoolOptions::new().max_connections(5).connect(&db_url).await?;
let p1 = pool.clone();
let p2 = pool.clone();
let api = Router::new()
.get("/tasks", move |req| controllers::tasks::list(req, p1.clone()))
.post("/tasks", move |req| controllers::tasks::create(req, p2.clone()));
let router = Router::new()
.mount("/api", api)
.static_files("/static", "./public")
.get("/", home_handler)
.use_middleware(LoggingMiddleware::new());
let app = App::new().router(router);
let addr: SocketAddr = format!("{}:{}", settings.server.host, settings.server.port).parse()?;
println!("âš¡ TaskManager running at http://{}", addr);
Server::new(app, addr).run().await?;
Ok(())
}
async fn home_handler(_req: Request) -> Result<Response, BoxError> {
let html = include_str!("../templates/index.html");
Ok(Response::new().html(html))
}
Step 7: Build Interactive Frontend (templates/index.html & public/js/app.js)
1. Update templates/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaskManager — Built with Yaiko</title>
<link rel="stylesheet" href="/static/css/main.css">
</head>
<body>
<div class="app">
<header class="header">
<h2>âš¡ TaskManager</h2>
</header>
<main class="main" style="max-width: 600px; margin: 2rem auto;">
<div class="glass" style="padding: 2rem; border-radius: 12px;">
<h3>Add New Task</h3>
<form id="task-form" style="display: flex; gap: 10px; margin-top: 1rem;">
<input type="text" id="task-input" placeholder="What needs to be done?" style="flex: 1; padding: 10px; border-radius: 6px; border: 1px solid #ccc;">
<button type="submit" class="btn btn--primary">Add Task</button>
</form>
<h3 style="margin-top: 2rem;">Tasks</h3>
<ul id="task-list" style="list-style: none; padding: 0; margin-top: 1rem;"></ul>
</div>
</main>
</div>
<div id="toast-container"></div>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="/static/js/core.js"></script>
<script src="/static/js/app.js"></script>
</body>
</html>
2. Update public/js/app.js:
$(document).ready(function() {
function loadTasks() {
$.get('/api/tasks', function(res) {
let list = $('#task-list');
list.empty();
if (res.tasks.length === 0) {
list.append('<li style="color: #888;">No tasks found. Create one above!</li>');
return;
}
res.tasks.forEach(function(task) {
list.append(
'<li style="padding: 10px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between;">' +
'<span>' + task.title + '</span>' +
'<span style="color: green;">✓</span>' +
'</li>'
);
});
});
}
$('#task-form').on('submit', function(e) {
e.preventDefault();
let title = $('#task-input').val().trim();
if (!title) return;
$.ajax({
url: '/api/tasks',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ title: title }),
success: function(res) {
Yaiko.ui.toast('Task added successfully! 🚀', 'success');
$('#task-input').val('');
loadTasks();
},
error: function(err) {
Yaiko.ui.toast('Failed to add task.', 'error');
}
});
});
loadTasks();
});
Step 8: Test Your Application
Run your dev server:
yaiko dev
- Open http://127.0.0.1:3000.
- Enter a task like
"Deploy Yaiko app to production"and click Add Task. - Notice the instant task insertion and toast notification!
Step 9: Write Integration Tests
Create tests/integration_test.rs:
use yaiko_core::{TestClient, json};
use crate::build_router; // extract router constructor
#[tokio::test]
async fn test_tasks_api() {
let client = TestClient::new(build_router());
// Test GET /api/tasks
let res = client.get("/api/tasks").await;
res.assert_status(200);
// Test POST /api/tasks
let res = client.post("/api/tasks", r#"{"title":"Test task"}"#).await;
res.assert_status(201);
res.assert_body_contains("Test task");
}
Run tests:
cargo test
Step 10: Production Release Build
Compile your app for maximum performance:
yaiko build --release
The optimized binary is located at ./target/release/taskmanager.
./target/release/taskmanager
Your fullstack Yaiko app is ready for production! 🚀