Debug School

rakesh kumar
rakesh kumar

Posted on

Build a High-Performance Vehicle Home Slider: Fast 9-Card Multi-Table Query with Partner/System Image Labels in Rust + React

step1:env configuration
This step sets the database connection, server bind address, and session security key. Keeping these values in .env helps you change settings without touching code. In production, always use a strong SESSION_KEY and correct DB credentials.

# .env
DATABASE_URL=mysql://user:password@127.0.0.1:3306/motoshare
BIND_ADDR=127.0.0.1:8080
SESSION_KEY=change-this-in-prod

DATABASE_URL=mysql://root:@127.0.0.1:3306/rust_crud
APP_BIND=127.0.0.1:8080
APP_NAME=Rust CRUD Dashboard
Enter fullscreen mode Exit fullscreen mode

Cargo Setup
Step 2: Cargo Dependencies Setup

Here we install the required Rust libraries for Actix-Web, SQLX MySQL, JSON handling, sessions, and image processing. These dependencies build the full backend foundation, so your API and DB queries work smoothly. It also ensures your app is ready for real-world features like file uploads and sessions.

[dependencies]
actix-files = "0.6.8"
actix-multipart = "0.7.2"
actix-cors = "0.7.1"
actix-session = { version = "0.10.1", features = ["cookie-session"] }
actix-web = "4.11.0"
chrono = { version = "0.4.40", features = ["serde"] }
dotenvy = "0.15.7"
futures-util = "0.3.31"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
sqlx = { version = "0.8.3", features = ["runtime-tokio-rustls", "mysql", "chrono"] }
tera = "1.20.0"
tokio = { version = "1.44.1", features = ["fs", "io-util"] }
uuid = { version = "1.16.0", features = ["v4"] }
image = { version = "0.25.5", default-features = false, features = ["jpeg", "png", "webp", "gif"] }
Enter fullscreen mode Exit fullscreen mode

Add Home DTO Models
src\models.rs
Step 3: Add Home DTO Models

These models define the exact data structure that the homepage slider needs. Instead of sending raw DB rows, we return clean and UI-ready objects like HomeVehicleCard and HomeVehicleImage. This keeps the frontend simple and avoids extra parsing work in React.


#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HomeVehicleImage {
    pub file: String,
    pub src: String, // "partner" | "default"
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HomeVehicleCard {
    pub id: u64,
    pub vehical: String,
    pub brand: String,
    pub model: String,
    pub city: String,
    pub price: String,
    pub dummy_data: String,
    pub images: Vec<HomeVehicleImage>,
}
Enter fullscreen mode Exit fullscreen mode

DB Query (Fast + 9 rows + joins)
Step 4: DB Query (Fast + 9 Rows + Multi Table Joins)

This query fetches only the latest active vehicles and limits results to 9 rows for maximum speed. We use joins so we don’t hit the database again and again for related data like brand, city, or shop info. This is the key step for building a high-performance homepage card section.
C:\xampp\htdocs\rust_crud_app\src\db\vehicle.rs
collections

use sqlx::MySqlPool;
use std::collections::HashSet;

use crate::models::{HomeVehicleCard, HomeVehicleImage, Vehicle, VehicleForm};
#[derive(Debug, sqlx::FromRow)]
struct HomeVehicleRow {
    id: u64,
    vehical: Option<String>,
    brand: Option<String>,
    model: Option<String>,
    city: Option<String>,
    price: Option<String>,
    dummy_data: Option<String>,
    vechicle_image: Option<String>,
}

Enter fullscreen mode Exit fullscreen mode

Step 5: Parse Images (Partner First, Then System Fallback)

Vehicle images can come from partner upload or system/admin upload, so we parse JSON safely. We also remove duplicates and pick a limited number of images so UI stays fast. This makes sure every card gets at least one image and never breaks the slider.
C:\xampp\htdocs\rust_crud_app\src\db\vehicle.rs

fn push_images(
    out: &mut Vec<HomeVehicleImage>,
    seen: &mut HashSet<String>,
    names: &[String],
    src: &str,
    max_count: usize,
) {
    for name in names.iter().take(max_count) {
        let clean = name.trim();
        if clean.is_empty() || clean.eq_ignore_ascii_case("null") {
            continue;
        }
        if !seen.insert(clean.to_string()) {
            continue;
        }
        out.push(HomeVehicleImage {
            file: clean.to_string(),
            src: src.to_string(),
        });
    }
}

fn parse_images(raw: Option<String>) -> Vec<HomeVehicleImage> {
    let txt = raw.unwrap_or_default();
    let text = txt.trim();
    if text.is_empty() {
        return Vec::new();
    }

    let mut out = Vec::new();
    let mut seen = HashSet::new();

    if let Ok(v) = serde_json::from_str::<serde_json::Value>(text) {
        if let Some(obj) = v.as_object() {
            let partner = obj
                .get("partner")
                .and_then(|x| x.as_array())
                .map(|arr| {
                    arr.iter()
                        .filter_map(|x| x.as_str().map(|s| s.to_string()))
                        .collect::<Vec<_>>()
                })
                .unwrap_or_default();
            let admin = obj
                .get("admin")
                .and_then(|x| x.as_array())
                .map(|arr| {
                    arr.iter()
                        .filter_map(|x| x.as_str().map(|s| s.to_string()))
                        .collect::<Vec<_>>()
                })
                .unwrap_or_default();

            // 1 partner + 4 system images for home cards.
            push_images(&mut out, &mut seen, &partner, "partner", 1);
            push_images(&mut out, &mut seen, &admin, "default", 4);
            return out;
        }
    }

    // Legacy fallback: single filename stored directly.
    let clean = text
        .trim_matches('"')
        .trim_matches('\'')
        .trim()
        .to_string();
    if !clean.is_empty() && !clean.eq_ignore_ascii_case("null") {
        out.push(HomeVehicleImage {
            file: clean,
            src: "default".to_string(),
        });
    }
    out
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Final Function That Returns Home Cards

This function executes the optimized query and converts each row into HomeVehicleCard. It also calls parse_images() so React receives a clean images[] list directly. This is the backend part that makes the homepage slider load instantly.
C:\xampp\htdocs\rust_crud_app\src\db\vehicle.rs


pub async fn list_home_vehicle_cards(
    pool: &MySqlPool,
    limit: i64,
) -> Result<Vec<HomeVehicleCard>, sqlx::Error> {
    let rows = sqlx::query_as::<_, HomeVehicleRow>(
        "SELECT
            av.id AS id,
            CAST(COALESCE(bm.vehical, av.vechicle) AS CHAR) AS vehical,
            CAST(COALESCE(av.brand, bm.brand) AS CHAR) AS brand,
            CAST(COALESCE(av.model, bm.model) AS CHAR) AS model,
            CAST(COALESCE(s.city, av.city) AS CHAR) AS city,
            CAST(COALESCE(av.price, '') AS CHAR) AS price,
            CAST(COALESCE(av.dummy_data, '0') AS CHAR) AS dummy_data,
            CAST(COALESCE(av.vechicle_image, '') AS CHAR) AS vechicle_image
         FROM addvechicles av
         LEFT JOIN addvehical_byadmins bm ON bm.id = av.vehical_id
         LEFT JOIN shops s ON s.id = av.shop_id
         LEFT JOIN users u ON u.id = av.vender_ID
         WHERE av.status = 'active'
         ORDER BY av.id DESC
         LIMIT ?",
    )
    .bind(limit)
    .fetch_all(pool)
    .await?;

    Ok(rows
        .into_iter()
        .map(|r| HomeVehicleCard {
            id: r.id,
            vehical: r.vehical.unwrap_or_default(),
            brand: r.brand.unwrap_or_default(),
            model: r.model.unwrap_or_default(),
            city: r.city.unwrap_or_default(),
            price: r.price.unwrap_or_default(),
            dummy_data: r.dummy_data.unwrap_or_else(|| "0".to_string()),
            images: parse_images(r.vechicle_image),
        })
        .collect())
}

Enter fullscreen mode Exit fullscreen mode

Export DB Function
rust_crud_app\src\db.rs
Exporting the function makes it accessible anywhere in the project without importing deep module paths. This keeps code cleaner and easier to maintain. It also helps handlers use DB functions directly.

pub use vehicle::{
    create_vehicle, delete_vehicle, get_vehicle_by_id, list_home_vehicle_cards, list_vehicles,
    update_vehicle,
};
Enter fullscreen mode Exit fullscreen mode

Add Handler
Step 8: Add API Handler (Controller Layer)

This handler calls the DB function and returns the response as JSON. It also wraps output inside a common response structure so frontend always gets the same format. This keeps your API consistent and professional.
rust_crud_app\src\handlers\vehicle.rs

use crate::db::{
    create_vehicle, delete_vehicle, get_vehicle_by_id, list_home_vehicle_cards, list_vehicles,
    update_vehicle, AppState,
};
use crate::models::VehicleForm;
use super::common::{compress_uploaded_image, ensure_login_user_id, ApiResponse};

pub async fn list_home_vehicle_cards_handler(state: web::Data<AppState>) -> Result<HttpResponse> {
    let cards = list_home_vehicle_cards(&state.pool, 9)
        .await
        .map_err(error::ErrorInternalServerError)?;

    let response = ApiResponse {
        message: "Home vehicles fetched".to_string(),
        data: cards,
    };

    Ok(HttpResponse::Ok().json(response))
}
Enter fullscreen mode Exit fullscreen mode

Export Handler
Step 9: Export Handler in handlers.rs

Just like DB export, handler export makes routing cleaner. This avoids importing handler functions from long file paths. It also keeps your project structure neat.
src\handlers.rs

pub use vehicle::{
    create_vehicle_handler,
    delete_vehicle_handler,
    get_vehicle_handler,
    list_home_vehicle_cards_handler,
    list_vehicles_handler,
    update_vehicle_handler,
    upload_vehicle_documents_handler,
    upload_vehicle_partner_image_handler,
};
Enter fullscreen mode Exit fullscreen mode

Add Route
Step 10: Add Route for Home Vehicles

This is where you connect the URL to the handler function. Once this route is added, frontend can call a dedicated endpoint for home slider cards. This avoids loading the full vehicle list on homepage.

// src/handlers.rs
pub use vehicle::list_home_vehicle_cards_handler;
Enter fullscreen mode Exit fullscreen mode

Step: main.rs Route Wiring (This is the missing step in your blog)

This step is where your backend actually exposes the API URL that React calls. Even if you wrote handler + DB code correctly, the endpoint won’t work until you register the route inside HttpServer::new(). In your case, because you are using web::scope("/api"), your real URL becomes /api/home/vehicles (not /home/vehicles).

✅ Your route is already correct in main.rs (keep it like this)
main.rs

.service(
    web::scope("/api")
        // ...
        .route("/home/vehicles", web::get().to(handlers::list_home_vehicle_cards_handler))
        // ...
)
Enter fullscreen mode Exit fullscreen mode

Frontend API Method
Step 11: Frontend API Method

This function calls the backend endpoint and returns home vehicles. Keeping it in a single API file helps reuse across multiple pages. It also keeps your React components clean.

// frontend/src/api.js
listHomeVehicles: () => request("/home/vehicles"),
Enter fullscreen mode Exit fullscreen mode

Load in App
Step 12: Load Vehicles in App (With Fallback)

Here we fetch the fast home endpoint first, and if it fails, we fallback to the normal vehicles list. This ensures homepage never breaks even if a new API is not deployed yet. Also, we keep it limited to 9 items for consistent UI.

// frontend/src/App.jsx
const [homeVehicles, setHomeVehicles] = useState([]);

const loadHomeVehicles = async () => {
  try {
    const result = await api.listHomeVehicles();
    setHomeVehicles(Array.isArray(result.data) ? result.data.slice(0, 9) : []);
  } catch {
    const fallback = await api.listVehicles();
    setHomeVehicles(Array.isArray(fallback.data) ? fallback.data.slice(0, 9) : []);
  }
};

// call on boot
await loadHomeVehicles();

// pass to Home
<HomePage vehicles={homeVehicles} ... />
Enter fullscreen mode Exit fullscreen mode

Render Home Section
Step 13: Render Home Section

This step passes the vehicle data into the homepage section component. Keeping homepage layout separate makes the code more organized. It also helps you modify UI later without touching API logic.

// frontend/src/pages/HomePage.jsx
import HomeAfterIntroSection from "../components/HomeAfterIntroSection";

export default function HomePage({ vehicles = [], ...props }) {
  return (
    <section>
      {/* hero/search */}
      <HomeAfterIntroSection vehicles={vehicles} />
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

Slider Component (auto-scroll + partner/system badge)

// frontend/src/components/HomeAfterIntroSection.jsx
// - parse v.images (new API)
// - fallback parse v.vechicle_image (legacy)
// - auto rotate every 3s
// - show badge: "uploaded by Partner" / "System photo"
Enter fullscreen mode Exit fullscreen mode

DB Indexes for Performance
Step 14: Slider Component (Auto Scroll + Source Badge)

This component is responsible for showing the slider UI and rotating items every few seconds. It also shows badge text like “uploaded by Partner” or “System photo” based on src. This improves trust and makes your UI feel more real and professional.

CREATE INDEX idx_addvechicles_home ON addvechicles (status, id);
CREATE INDEX idx_addvechicles_fk1 ON addvechicles (vehical_id);
CREATE INDEX idx_addvechicles_fk2 ON addvechicles (shop_id);
CREATE INDEX idx_addvechicles_fk3 ON addvechicles (vender_ID);
CREATE INDEX idx_addvehical_byadmins_id ON addvehical_byadmins (id);
CREATE INDEX idx_shops_id ON shops (id);
CREATE INDEX idx_users_id ON users (id);
Enter fullscreen mode Exit fullscreen mode
cargo run
# frontend
npm run dev
Enter fullscreen mode Exit fullscreen mode

Top comments (0)