Debug School

rakesh kumar
rakesh kumar

Posted on

From Laravel to Rust: Building a Hybrid SSR + React Architecture for Ultra-Fast APIs

Laravel + Blade (Hybrid Mount)
React (Vite) – Full Folder Structure + Full Code
Rust API – Full Folder Structure + Full Code (Axum + SQLx)
Rust as a separate API service (recommended)

Below is a complete, step-by-step, no-missing-parts setup for Hybrid SSR (Laravel Blade page-1) + React UI + Rust API (page-2+) with a clean folder structure for both React (inside Laravel Vite) and Rust (Axum + SQLx).

I’m assuming:

Laravel is serving the Home page Blade.
React is bundled via Vite.
Rust will serve: GET /api/home/vehicles?page=2&per_page=9
Images live at: /storage/vechile_image/{file}
Enter fullscreen mode Exit fullscreen mode

Laravel + Blade (Hybrid Mount)

A) Laravel + Blade (Hybrid Mount)
A1) .env (Laravel)

VEHICLES_API_BASE=https://rust-api.yourdomain.com
Enter fullscreen mode Exit fullscreen mode

A2) config/app.php

Add this inside the returned array:

'vehicles_api_base' => env('VEHICLES_API_BASE', ''),
Enter fullscreen mode Exit fullscreen mode

A3) Controller (THIS WAS MISSING)

Create a controller method that:

Calls Rust API for page=1

Converts response into LengthAwarePaginator

Passes $data to Blade
Enter fullscreen mode Exit fullscreen mode

app/Http/Controllers/HomeController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Http;

class HomeController extends Controller
{
    public function index(Request $request)
    {
        $apiBase = rtrim(config('app.vehicles_api_base') ?? '', '/');
        $endpoint = '/api/home/vehicles';

        $page = 1;        // SSR loads page 1
        $perPage = 9;

        $items = [];
        $total = 0;
        $currentPage = 1;
        $lastPage = 1;

        $res = Http::acceptJson()
            ->timeout(10)
            ->get($apiBase . $endpoint, [
                'page' => $page,
                'per_page' => $perPage,
            ]);

        if ($res->ok()) {
            $json = $res->json();

            // expected Rust shape:
            // { ok: true, data: [...], pagination: { current_page, last_page, per_page, total } }
            if (($json['ok'] ?? false) === true) {
                $items = $json['data'] ?? [];

                $pg = $json['pagination'] ?? [];
                $currentPage = (int)($pg['current_page'] ?? 1);
                $lastPage    = (int)($pg['last_page'] ?? 1);
                $perPage     = (int)($pg['per_page'] ?? $perPage);
                $total       = (int)($pg['total'] ?? count($items));
            }
        }

        // Convert to Laravel paginator so Blade can call items(), currentPage(), lastPage()
        $data = new LengthAwarePaginator(
            $items,
            $total,
            $perPage,
            $currentPage,
            [
                'path' => $request->url(),
                'query' => $request->query(),
            ]
        );

        return view('home', compact('data'));
    }
}
Enter fullscreen mode Exit fullscreen mode

A4) Route

✅ routes/web.php

use App\Http\Controllers\HomeController;

Route::get('/', [HomeController::class, 'index']);
Enter fullscreen mode Exit fullscreen mode

A5) Blade file (home/welcome/dashboard)

✅ Now your Blade is correct (because $data exists):

@php
  $apiBase = config('app.vehicles_api_base');
  $apiBase = $apiBase ? rtrim($apiBase, '/') : '';
@endphp

<div id="HomeVehiclesSection"
     data-api-base="{{ $apiBase }}"
     data-endpoint="/api/home/vehicles"
     data-initial='@json($data->items())'
     data-page="{{ $data->currentPage() }}"
     data-last-page="{{ $data->lastPage() }}">
</div>

@vite('resources/js/appss.jsx')

Enter fullscreen mode Exit fullscreen mode

✅ This gives React:

initial vehicles (data-initial)

rust base URL (data-api-base)

endpoint (data-endpoint)

✅ B)

React (Vite) – Full Folder Structure + Full Code

B1) Folder structure (inside Laravel)

resources/js/appss.jsx (mount)

import React from "react";
import { createRoot } from "react-dom/client";
import HomeVehiclesSection from "./components/HomeVehiclesSection/HomeVehiclesSection";

document.addEventListener("DOMContentLoaded", () => {
  const el = document.getElementById("HomeVehiclesSection");
  if (!el) return;

  const props = {
    apiBase: el.dataset.apiBase || "",
    endpoint: el.dataset.endpoint || "/api/home/vehicles",
    initialData: el.dataset.initial ? JSON.parse(el.dataset.initial) : [],
    initialPage: parseInt(el.dataset.page || "1", 10),
    initialLastPage: parseInt(el.dataset.lastPage || "1", 10),
  };

  if (!el.__reactRoot) {
    const root = createRoot(el);
    root.render(<HomeVehiclesSection {...props} />);
    el.__reactRoot = root;
  }
});
Enter fullscreen mode Exit fullscreen mode

B3) resources/js/components/utils/slug.js

export function slugify(s = "") {
  return s
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[^a-zA-Z0-9\s-]/g, "")
    .trim()
    .replace(/\s+/g, "-")
    .toLowerCase();
}
Enter fullscreen mode Exit fullscreen mode

B4) resources/js/components/HomeVehiclesSection/index.js

export { default } from "./HomeVehiclesSection";
Enter fullscreen mode Exit fullscreen mode

B5) resources/js/components/HomeVehiclesSection/HomeVehiclesSection.jsx

✅ Hybrid logic:

Page-1 uses SSR initialData

Page-2+ loads from Rust API using Load More

Prevents missing/blank UI

import React, { useMemo, useState } from "react";
import VehicleCard from "./VehicleCard";

export default function HomeVehiclesSection({
  apiBase,
  endpoint,
  initialData,
  initialPage,
  initialLastPage,
}) {
  const [items, setItems] = useState(Array.isArray(initialData) ? initialData : []);
  const [page, setPage] = useState(Number(initialPage || 1));
  const [lastPage, setLastPage] = useState(Number(initialLastPage || 1));
  const [loading, setLoading] = useState(false);
  const [err, setErr] = useState("");

  const apiUrl = useMemo(() => {
    const base = (apiBase || "").replace(/\/$/, "");
    return base ? `${base}${endpoint}` : endpoint;
  }, [apiBase, endpoint]);

  const loadMore = async () => {
    if (loading) return;
    if (page >= lastPage) return;

    setLoading(true);
    setErr("");

    try {
      const nextPage = page + 1;
      const res = await fetch(`${apiUrl}?page=${nextPage}&per_page=9`, {
        headers: { Accept: "application/json" },
      });

      const json = await res.json();
      if (!res.ok || !json?.ok) throw new Error(json?.message || "API failed");

      setItems((prev) => [...prev, ...(json.data || [])]);
      setPage(json.pagination?.current_page || nextPage);
      setLastPage(json.pagination?.last_page || lastPage);
    } catch (e) {
      setErr(e?.message || "Failed to load vehicles");
    } finally {
      setLoading(false);
    }
  };

  return (
    <section className="section-padding gray-bg" style={{ marginTop: 25 }}>
      <div className="containerrrr">
        <div className="vehicle-grid">
          {items.map((v) => (
            <VehicleCard key={v.vechicle_id} v={v} />
          ))}
        </div>

        {err && (
          <div className="text-center" style={{ marginTop: 18, color: "#b91c1c" }}>
            {err}
          </div>
        )}

        {page < lastPage && (
          <div className="text-center" style={{ marginTop: 25 }}>
            <button className="btn btn-primary" disabled={loading} onClick={loadMore}>
              {loading ? "Loading..." : "Load More Vehicles"}
            </button>
          </div>
        )}
      </div>
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

B6) resources/js/components/HomeVehiclesSection/VehicleSlider.jsx

✅ React slider (no jQuery). Supports partner/default badge.

import React, { useEffect, useRef, useState } from "react";

export default function VehicleSlider({ images = [], intervalMs = 3000 }) {
  const [idx, setIdx] = useState(0);
  const timer = useRef(null);

  const hasMany = images.length > 1;
  const currentSrc = (images[idx]?.src || "default").toLowerCase();

  const stop = () => {
    if (timer.current) clearInterval(timer.current);
    timer.current = null;
  };

  const start = () => {
    stop();
    if (!hasMany) return;
    timer.current = setInterval(() => {
      setIdx((p) => (p + 1) % images.length);
    }, intervalMs);
  };

  useEffect(() => {
    start();
    return () => stop();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [images.length]);

  return (
    <div className="veh-slider" data-index={idx}
         onMouseEnter={stop}
         onMouseLeave={start}
         onTouchStart={stop}
         onTouchEnd={start}>
      <div className={`veh-photo-badge ${currentSrc === "partner" ? "is-partner" : "is-default"}`}>
        <span className="dot"></span>
        <span className="txt"></span>
      </div>

      <div className="veh-slides" style={{ transform: `translateX(-${idx * 100}%)` }}>
        {images.map((img, i) => (
          <div className="veh-slide" key={i} data-source={img.src}>
            <img
              src={img.url}
              className="vehicle-card-img"
              alt="vehicle"
              loading="lazy"
            />
          </div>
        ))}
      </div>

      {hasMany && (
        <>
          <button className="veh-nav veh-prev" type="button"
                  onClick={(e) => { e.preventDefault(); e.stopPropagation(); setIdx((p) => (p - 1 + images.length) % images.length); start(); }}>
            ‹
          </button>
          <button className="veh-nav veh-next" type="button"
                  onClick={(e) => { e.preventDefault(); e.stopPropagation(); setIdx((p) => (p + 1) % images.length); start(); }}>
            ›
          </button>

          <div className="veh-dots" aria-hidden="true">
            {images.map((_, i) => (
              <button key={i} type="button"
                      className={`veh-dot ${i === idx ? "is-active" : ""}`}
                      onClick={(e) => { e.preventDefault(); e.stopPropagation(); setIdx(i); start(); }} />
            ))}
          </div>
        </>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

B7) resources/js/components/HomeVehiclesSection/VehicleCard.jsx

✅ Uses your same CSS classes.

import React from "react";
import VehicleSlider from "./VehicleSlider";
import { slugify } from "../utils/slug";

export default function VehicleCard({ v }) {
  const images = Array.isArray(v.images_meta) && v.images_meta.length
    ? v.images_meta
    : [{
        src: "default",
        file: "",
        url: "https://bd.gaadicdn.com/processedimages/yamaha/mt-15-2-0/source/mt-15-2-06613f885e681c.jpg",
      }];

  const vehicleLower = String(v.vehical || "").trim().toLowerCase();

  const cityHref = `/${slugify(v.city || "")}/${
    vehicleLower === "car" ? "car-rentals" :
    vehicleLower === "bike" ? "bike-rentals" :
    "bicycle-rentals"
  }`;

  const brandHref = `/${slugify(vehicleLower)}s/${slugify(v.brand || "")}`;

  return (
    <div className="col-list-3">
      <div className="vehicle-card">

        <div className="vehicle-image">
          <a href={v.show_url}>
            <VehicleSlider images={images} />
          </a>

          <div className="vehicle-specs">
            <ul>
              <li><i className="fa fa-car"></i> Vehicle Type: <span>{v.vehical}</span></li>
              <li><i className="fa fa-car"></i> Model: <span>{v.model}</span></li>
            </ul>
          </div>
        </div>

        <div className="rating-badge-container">
          <div className="rating-badge">
            {v.rating_10 && v.comment_count ? (
              <a href={v.review_url} className="rating-link" style={{ textDecoration: "none" }}>
                <i className="fa fa-star"></i>
                <span className="rating-score">{Number(v.rating_10).toFixed(1)}/10</span>
                <span className="rating-votes">
                  {Number(v.rating_count || 0)} Ratings • {Number(v.comment_count || 0)} Comments
                </span>
              </a>
            ) : (
              <span className="no-rating">No ratings yet</span>
            )}
          </div>
          <a href={v.shop_url} className="view-shop-link">View Shop & Reviews</a>
        </div>

        <div className="vehicle-title">
          <h6>
            Brand:{" "}
            <a href={brandHref} className="brand-link">
              {v.brand}
            </a>
          </h6>
          <span className="price">{v.price} {v.currency_code || ""}/Day</span>
        </div>

        <div className="vehicle-footer">
          <div className="location">
            <p>City: <a href={cityHref} className="city-link">{v.city}</a></p>
          </div>

          <div className="booking-action">
            <a href={v.booking_url}>
              <button className="book-btn">Book</button>
            </a>
          </div>
        </div>

      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

B8) Build

npm install
npm run dev
# or production:
npm run build

Enter fullscreen mode Exit fullscreen mode

Rust API – Full Folder Structure + Full Code (Axum + SQLx)

C1) Rust folder structure

C2) Cargo.toml

[package]
name = "motoshare_api"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
dotenvy = "0.15"
sqlx = { version = "0.7", features = ["runtime-tokio", "mysql", "macros"] }
tower-http = { version = "0.5", features = ["cors", "compression-gzip"] 
Enter fullscreen mode Exit fullscreen mode

}

C3) .env (Rust)

DATABASE_URL=mysql://user:pass@127.0.0.1:3306/motoshare
PUBLIC_BASE_URL=https://motoshare.in
ASSET_BASE_URL=https://motoshare.in
PORT=8080
ALLOWED_ORIGIN=https://motoshare.in
Enter fullscreen mode Exit fullscreen mode

C4) src/config.rs

use std::env;

#[derive(Clone)]
pub struct AppConfig {
    pub database_url: String,
    pub public_base_url: String,
    pub asset_base_url: String,
    pub allowed_origin: String,
}

impl AppConfig {
    pub fn from_env() -> Self {
        Self {
            database_url: env::var("DATABASE_URL").expect("DATABASE_URL missing"),
            public_base_url: env::var("PUBLIC_BASE_URL").expect("PUBLIC_BASE_URL missing"),
            asset_base_url: env::var("ASSET_BASE_URL").expect("ASSET_BASE_URL missing"),
            allowed_origin: env::var("ALLOWED_ORIGIN").unwrap_or_else(|_| "*".to_string()),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

C5) src/db.rs

use sqlx::{MySql, Pool};
pub type Db = Pool<MySql>;

pub async fn connect(database_url: &str) -> Db {
    Pool::<MySql>::connect(database_url).await.expect("DB connect failed")
}
Enter fullscreen mode Exit fullscreen mode

C6) s*rc/utils/slug.rs*

pub fn slugify(s: &str) -> String {
    let s = s.trim().to_lowercase();
    let mut out = String::new();
    let mut dash = false;

    for ch in s.chars() {
        if ch.is_ascii_alphanumeric() {
            out.push(ch);
            dash = false;
        } else if ch.is_whitespace() || ch == '-' {
            if !dash && !out.is_empty() {
                out.push('-');
                dash = true;
            }
        }
    }
    while out.ends_with('-') { out.pop(); }
    out
}
Enter fullscreen mode Exit fullscreen mode

C7) src/utils/images.rs

use serde_json::Value;
use std::collections::HashSet;

#[derive(Clone)]
pub struct ImageMetaBuilt {
    pub src: String,
    pub file: String,
}

pub fn build_images_meta(json_str: Option<&str>) -> Vec<ImageMetaBuilt> {
    let mut partner: Vec<String> = vec![];
    let mut admin: Vec<String> = vec![];

    if let Some(s) = json_str {
        if let Ok(v) = serde_json::from_str::<Value>(s) {
            if let Some(a) = v.get("partner").and_then(|x| x.as_array()) {
                partner = a.iter().filter_map(|x| x.as_str().map(|z| z.to_string())).collect();
            }
            if let Some(a) = v.get("admin").and_then(|x| x.as_array()) {
                admin = a.iter().filter_map(|x| x.as_str().map(|z| z.to_string())).collect();
            }
        }
    }

    let clean = |arr: Vec<String>| {
        arr.into_iter().filter(|x| {
            let t = x.trim();
            !t.is_empty() && t != "null"
        }).collect::<Vec<_>>()
    };

    partner = clean(partner);
    admin = clean(admin);

    let mut seen: HashSet<String> = HashSet::new();
    let mut out: Vec<ImageMetaBuilt> = vec![];

    for f in partner {
        if seen.insert(f.clone()) {
            out.push(ImageMetaBuilt { src: "partner".to_string(), file: f });
        }
    }
    for f in admin {
        if seen.insert(f.clone()) {
            out.push(ImageMetaBuilt { src: "default".to_string(), file: f });
        }
    }

    out
}
Enter fullscreen mode Exit fullscreen mode

C8) src/utils/urls.rs

use crate::utils::slug::slugify;

pub fn image_url(asset_base: &str, file: &str) -> String {
    format!("{}/storage/vechile_image/{}", asset_base.trim_end_matches('/'), file)
}

pub fn shop_url(base: &str, partner_slug: &str, shop_slug: &str) -> String {
    format!("{}/partners/{}/{}", base.trim_end_matches('/'), partner_slug, shop_slug)
}

pub fn review_url(base: &str, partner_slug: &str, shop_slug: &str) -> String {
    format!("{}/partners/{}/{}/review", base.trim_end_matches('/'), partner_slug, shop_slug)
}

pub fn booking_url(base: &str, id: i64) -> String {
    format!("{}/booking/{}", base.trim_end_matches('/'), id)
}

// Adjust this to your exact show route pattern if needed
pub fn show_vehicle_url(base: &str, city: &str, vehicle_type: &str, brand: &str, model: &str, id: i64) -> String {
    format!(
        "{}/{}/{}/{}/{}/{}",
        base.trim_end_matches('/'),
        slugify(city),
        slugify(vehicle_type),
        slugify(brand),
        slugify(model),
        id
    )
}
Enter fullscreen mode Exit fullscreen mode

C9) src/models/home_vehicles.rs

use serde::Serialize;

#[derive(Serialize)]
pub struct Pagination {
    pub current_page: u32,
    pub last_page: u32,
    pub per_page: u32,
    pub total: u64,
}

#[derive(Serialize)]
pub struct ApiResponse<T> {
    pub ok: bool,
    pub data: Vec<T>,
    pub pagination: Pagination,
}

#[derive(Serialize)]
pub struct ImageMeta {
    pub src: String,
    pub file: String,
    pub url: String,
}

#[derive(Serialize)]
pub struct VehicleItem {
    pub vechicle_id: i64,
    pub brand: Option<String>,
    pub model: Option<String>,
    pub vehical: Option<String>,
    pub city: Option<String>,
    pub price: Option<String>,

    pub currency_code: Option<String>,
    pub has_partner_photos: bool,

    pub images_meta: Vec<ImageMeta>,
    pub main_image: Option<String>,

    pub slug: Option<String>,
    pub shop_slug: Option<String>,

    pub shop_url: String,
    pub review_url: String,
    pub show_url: String,
    pub booking_url: String,

    pub rating_10: Option<f32>,
    pub rating_count: Option<i64>,
    pub comment_count: Option<i64>,
}
Enter fullscreen mode Exit fullscreen mode

C10) src/services/home_vehicles_service.rs (FULL QUERY + TRANSFORM)

use sqlx::Row;

use crate::{
    config::AppConfig,
    db::Db,
    models::home_vehicles::{VehicleItem, ImageMeta},
    utils::{images::build_images_meta, urls},
};

pub async fn fetch_home_vehicles(
    db: &Db,
    cfg: &AppConfig,
    page: u32,
    per_page: u32,
) -> Result<(Vec<VehicleItem>, u64), sqlx::Error> {
    let offset = (page.saturating_sub(1) * per_page) as i64;
    let limit = per_page as i64;

    let total_row = sqlx::query(
        r#"
        SELECT COUNT(*) as c
        FROM addvechicles
        WHERE status='active' AND publish='1' AND dummy_data='0'
        "#
    )
    .fetch_one(db)
    .await?;
    let total: i64 = total_row.try_get("c").unwrap_or(0);
    let total_u64 = total.max(0) as u64;

    let rows = sqlx::query(
        r#"
        SELECT
            av.id as vechicle_id,
            av.status, av.publish, av.dummy_data, av.vehical, av.price, av.vechicle_image,
            aba.brand, aba.model,
            s.city, s.shop_slug,
            u.slug
        FROM addvechicles av
        JOIN addvehical_byadmins aba ON av.vehical_id = aba.id
        JOIN shops s ON av.shop_id = s.id
        JOIN users u ON av.vender_ID = u.id
        WHERE av.status='active' AND av.publish='1' AND av.dummy_data='0'
        ORDER BY av.id DESC
        LIMIT ? OFFSET ?
        "#
    )
    .bind(limit)
    .bind(offset)
    .fetch_all(db)
    .await?;

    let mut out = Vec::with_capacity(rows.len());

    for r in rows {
        let vechicle_id: i64 = r.try_get("vechicle_id").unwrap_or(0);

        let brand: Option<String> = r.try_get("brand").ok();
        let model: Option<String> = r.try_get("model").ok();
        let vehical: Option<String> = r.try_get("vehical").ok();
        let city: Option<String> = r.try_get("city").ok();
        let price: Option<String> = r.try_get("price").ok();
        let shop_slug: Option<String> = r.try_get("shop_slug").ok();
        let slug: Option<String> = r.try_get("slug").ok();
        let vechicle_image_json: Option<String> = r.try_get("vechicle_image").ok();

        let built = build_images_meta(vechicle_image_json.as_deref());
        let has_partner_photos = built.iter().any(|x| x.src == "partner");

        let images_meta: Vec<ImageMeta> = built
            .iter()
            .map(|x| ImageMeta {
                src: x.src.clone(),
                file: x.file.clone(),
                url: urls::image_url(&cfg.asset_base_url, &x.file),
            })
            .collect();

        let main_image = images_meta.get(0).map(|x| x.url.clone());

        let partner_slug = slug.clone().unwrap_or_default();
        let shop_slug_s = shop_slug.clone().unwrap_or_default();

        let shop_url = urls::shop_url(&cfg.public_base_url, &partner_slug, &shop_slug_s);
        let review_url = urls::review_url(&cfg.public_base_url, &partner_slug, &shop_slug_s);
        let show_url = urls::show_vehicle_url(
            &cfg.public_base_url,
            city.as_deref().unwrap_or(""),
            vehical.as_deref().unwrap_or(""),
            brand.as_deref().unwrap_or(""),
            model.as_deref().unwrap_or(""),
            vechicle_id,
        );
        let booking_url = urls::booking_url(&cfg.public_base_url, vechicle_id);

        out.push(VehicleItem {
            vechicle_id,
            brand,
            model,
            vehical,
            city,
            price,
            currency_code: None,
            has_partner_photos,
            images_meta,
            main_image,
            slug,
            shop_slug,
            shop_url,
            review_url,
            show_url,
            booking_url,
            rating_10: None,
            rating_count: None,
            comment_count: None,
        });
    }

    Ok((out, total_u64))
}
Enter fullscreen mode Exit fullscreen mode

C11) src/handlers/home_vehicles.rs

use axum::{extract::{Query, State}, Json};
use serde::Deserialize;

use crate::{
    config::AppConfig,
    db::Db,
    models::home_vehicles::{ApiResponse, Pagination, VehicleItem},
    services::home_vehicles_service::fetch_home_vehicles,
};

#[derive(Clone)]
pub struct AppState {
    pub db: Db,
    pub cfg: AppConfig,
}

#[derive(Deserialize)]
pub struct VehiclesQuery {
    pub page: Option<u32>,
    pub per_page: Option<u32>,
}

pub async fn index(
    State(state): State<AppState>,
    Query(q): Query<VehiclesQuery>,
) -> Json<ApiResponse<VehicleItem>> {
    let per_page = q.per_page.unwrap_or(9).max(1).min(50);
    let page = q.page.unwrap_or(1).max(1);

    let (data, total) = fetch_home_vehicles(&state.db, &state.cfg, page, per_page)
        .await
        .unwrap_or((vec![], 0));

    let last_page = ((total as f64) / (per_page as f64)).ceil().max(1.0) as u32;

    Json(ApiResponse {
        ok: true,
        data,
        pagination: Pagination {
            current_page: page,
            last_page,
            per_page,
            total,
        },
    })
}
Enter fullscreen mode Exit fullscreen mode

C12) src/routes.rs

use axum::{routing::get, Router};
use crate::handlers::home_vehicles::{self, AppState};

pub fn build_router(state: AppState) -> Router {
    Router::new()
        .route("/api/home/vehicles", get(home_vehicles::index))
        .with_state(state)
}
Enter fullscreen mode Exit fullscreen mode

C13) src/main.rs (CORS + gzip)

mod config;
mod db;
mod routes;

mod handlers {
    pub mod home_vehicles;
}
mod models {
    pub mod home_vehicles;
}
mod services {
    pub mod home_vehicles_service;
}
mod utils {
    pub mod slug;
    pub mod images;
    pub mod urls;
}

use dotenvy::dotenv;
use std::{env, net::SocketAddr};

use tower_http::{
    cors::{CorsLayer, Any},
    compression::CompressionLayer,
};

use crate::{config::AppConfig, handlers::home_vehicles::AppState};

#[tokio::main]
async fn main() {
    dotenv().ok();

    let cfg = AppConfig::from_env();
    let db = db::connect(&cfg.database_url).await;

    let state = AppState { db, cfg: cfg.clone() };

    let cors = if cfg.allowed_origin == "*" {
        CorsLayer::new().allow_origin(Any).allow_methods(Any).allow_headers(Any)
    } else {
        CorsLayer::new()
            .allow_origin(cfg.allowed_origin.parse().unwrap())
            .allow_methods(Any)
            .allow_headers(Any)
    };

    let app = routes::build_router(state)
        .layer(cors)
        .layer(CompressionLayer::new());

    let port: u16 = env::var("PORT").ok().and_then(|v| v.parse().ok()).unwrap_or(8080);
    let addr: SocketAddr = format!("0.0.0.0:{}", port).parse().unwrap();

    println!("Rust API running at {}", addr);
    axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app)
        .await
        .unwrap();
}
Enter fullscreen mode Exit fullscreen mode

C14) Run Rust

cd rust-backend
cargo run
Enter fullscreen mode Exit fullscreen mode

Test:

curl "http://127.0.0.1:8080/api/home/vehicles?page=1&per_page=9"

✅ D) Final Step: Connect React to Rust

Set in Laravel .env:

VEHICLES_API_BASE=https://rust-api.yourdomain.com
Enter fullscreen mode Exit fullscreen mode

Now React loads page 2+ from Rust automatically.

✅ E) Performance Must-Do (DB Index)

Run in MySQL:

ALTER TABLE addvechicles
  ADD INDEX idx_av_status_publish_dummy_id (status, publish, dummy_data, id),
  ADD INDEX idx_av_vehical_id (vehical_id),
  ADD INDEX idx_av_shop_id (shop_id),
  ADD INDEX idx_av_vender_id (vender_ID);
Enter fullscreen mode Exit fullscreen mode

Rust as a separate API service (recommended)

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

public function vehiclesFromRust(Request $request)
{
    $page = (int) $request->query('page', 1);
    $per  = (int) $request->query('per_page', 9);

    $base = rtrim(config('app.vehicles_api_base'), '/');
    $url  = $base . '/api/home/vehicles';

    $res = Http::acceptJson()
        ->timeout(5)
        ->get($url, ['page' => $page, 'per_page' => $per]);

    if (!$res->successful()) {
        return response()->json([
            'ok' => false,
            'message' => 'Rust API failed',
            'status' => $res->status(),
        ], 502);
    }

    return response()->json($res->json(), 200);
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)