Clean Rust Backend Folder Structure (handlers / services / repos)
React CRUD Application with Rust (Axum) Backend and Laravel Blade Integration
Clean Rust Backend Folder Structure (handlers / services / repos)
What Each Folder Means (Simple & Practical)
handlers/ (HTTP layer)
Purpose: Handle HTTP details only
Parse request body/query/path params
Call the service layer
Convert service result into HTTP response
Keep handlers thin.
services/ (Business logic layer)
Purpose: Rules + workflows
Validate business rules (not just syntax)
Decide what repo calls are needed
Orchestrate operations (create user, send event, etc.)
This is the “brain” of your application.
repos/ (Data access layer)
Purpose: Database/Cache access only
CRUD queries
Transactions
Mapping DB rows to models
No business rules here.
Step-by-Step Implementation Example (Minimal, Realistic)
Below is a small example of a User Create + Get API using this structure.
1) models/user.rs
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct User {
pub id: String,
pub name: String,
pub email: String,
}
2) dto/user_dto.rs (request/response bodies)
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
pub name: String,
pub email: String,
}
#[derive(Debug, Serialize)]
pub struct ApiResponse<T> {
pub ok: bool,
pub data: Option<T>,
pub error: Option<String>,
}
3) errors/api_error.rs (central API error)
use axum::http::StatusCode;
#[derive(Debug)]
pub struct ApiError {
pub status: StatusCode,
pub message: String,
}
impl ApiError {
pub fn bad_request(msg: impl Into<String>) -> Self {
Self { status: StatusCode::BAD_REQUEST, message: msg.into() }
}
pub fn not_found(msg: impl Into<String>) -> Self {
Self { status: StatusCode::NOT_FOUND, message: msg.into() }
}
}
4) repos/user_repo.rs (in-memory example)
Later you replace this with DB (Postgres/MySQL) without touching handlers much.
use std::{collections::HashMap, sync::{Arc, RwLock}};
use crate::models::user::User;
#[derive(Clone)]
pub struct UserRepo {
users: Arc<RwLock<HashMap<String, User>>>,
}
impl UserRepo {
pub fn new() -> Self {
Self { users: Arc::new(RwLock::new(HashMap::new())) }
}
pub fn insert(&self, user: User) {
let mut map = self.users.write().unwrap();
map.insert(user.id.clone(), user);
}
pub fn find_by_id(&self, id: &str) -> Option<User> {
let map = self.users.read().unwrap();
map.get(id).cloned()
}
}
services/user_service.rs (business logic)
use uuid::Uuid;
use crate::{
dto::user_dto::CreateUserRequest,
errors::api_error::ApiError,
models::user::User,
repos::user_repo::UserRepo,
};
#[derive(Clone)]
pub struct UserService {
repo: UserRepo,
}
impl UserService {
pub fn new(repo: UserRepo) -> Self {
Self { repo }
}
pub fn create_user(&self, req: CreateUserRequest) -> Result<User, ApiError> {
let name = req.name.trim();
let email = req.email.trim();
if name.is_empty() {
return Err(ApiError::bad_request("Name is required"));
}
if !email.contains('@') {
return Err(ApiError::bad_request("Email looks invalid"));
}
let user = User {
id: Uuid::new_v4().to_string(),
name: name.to_string(),
email: email.to_string(),
};
self.repo.insert(user.clone());
Ok(user)
}
pub fn get_user(&self, id: &str) -> Result<User, ApiError> {
self.repo
.find_by_id(id)
.ok_or_else(|| ApiError::not_found("User not found"))
}
}
state/app_state.rs (shared app dependencies)
use crate::{repos::user_repo::UserRepo, services::user_service::UserService};
#[derive(Clone)]
pub struct AppState {
pub user_service: UserService,
}
impl AppState {
pub fn new() -> Self {
let user_repo = UserRepo::new();
let user_service = UserService::new(user_repo);
Self { user_service }
}
}
handlers/user_handler.rs (HTTP glue only)
use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use crate::{
dto::user_dto::{ApiResponse, CreateUserRequest},
state::app_state::AppState,
};
pub async fn create_user(
State(state): State<AppState>,
Json(req): Json<CreateUserRequest>,
) -> (StatusCode, Json<ApiResponse<crate::models::user::User>>) {
match state.user_service.create_user(req) {
Ok(user) => (
StatusCode::CREATED,
Json(ApiResponse { ok: true, data: Some(user), error: None }),
),
Err(e) => (
e.status,
Json(ApiResponse { ok: false, data: None, error: Some(e.message) }),
),
}
}
pub async fn get_user(
State(state): State<AppState>,
Path(id): Path<String>,
) -> (StatusCode, Json<ApiResponse<crate::models::user::User>>) {
match state.user_service.get_user(&id) {
Ok(user) => (
StatusCode::OK,
Json(ApiResponse { ok: true, data: Some(user), error: None }),
),
Err(e) => (
e.status,
Json(ApiResponse { ok: false, data: None, error: Some(e.message) }),
),
}
}
handlers/health_handler.rs
use axum::{http::StatusCode, Json};
use crate::dto::user_dto::ApiResponse;
pub async fn health() -> (StatusCode, Json<ApiResponse<&'static str>>) {
(
StatusCode::OK,
Json(ApiResponse { ok: true, data: Some("ok"), error: None }),
)
}
routes.rs (route wiring)
use axum::{routing::{get, post}, Router};
use crate::{handlers::{health_handler, user_handler}, state::app_state::AppState};
pub fn create_router(state: AppState) -> Router {
Router::new()
.route("/health", get(health_handler::health))
.route("/users", post(user_handler::create_user))
.route("/users/:id", get(user_handler::get_user))
.with_state(state)
}
main.rs (bootstrap)
mod routes;
mod handlers;
mod services;
mod repos;
mod models;
mod dto;
mod errors;
mod state;
use crate::state::app_state::AppState;
#[tokio::main]
async fn main() {
let state = AppState::new();
let app = routes::create_router(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
React CRUD Application with Rust (Axum) Backend and Laravel Blade Integration
Below is a simple, optimized CRUD for Vehicle with:
Database Table (MySQL)
Run this once:
CREATE TABLE vehicles (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(150) NOT NULL,
brand VARCHAR(100) NULL,
model VARCHAR(100) NULL,
price_per_day DECIMAL(10,2) NOT NULL DEFAULT 0,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
B) Backend (Rust) — Folder Structure
backend/
├── Cargo.toml
├── .env
└── src/
├── main.rs
├── app.rs
├── config.rs
├── db.rs
├── state.rs
├── models/
│ ├── mod.rs
│ └── vehicle.rs
├── repositories/
│ ├── mod.rs
│ └── vehicle_repo.rs
├── handlers/
│ ├── mod.rs
│ └── vehicle_handler.rs
└── routes/
├── mod.rs
└── vehicle_routes.rs
1) backend/Cargo.toml
[package]
name = "backend"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.7", features = ["mysql", "runtime-tokio", "macros", "chrono"] }
dotenvy = "0.15"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
2) backend/.env
DATABASE_URL=mysql://DB_USER:DB_PASS@127.0.0.1:3306/DB_NAME
HOST=127.0.0.1
PORT=3000
3) backend/src/main.rs
mod app;
mod config;
mod db;
mod state;
mod models;
mod repositories;
mod handlers;
mod routes;
#[tokio::main]
async fn main() {
app::run().await;
}
4) backend/src/config.rs
use std::env;
#[derive(Clone)]
pub struct Config {
pub database_url: String,
pub host: String,
pub port: u16,
}
impl Config {
pub fn from_env() -> Self {
dotenvy::dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL missing");
let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let port: u16 = env::var("PORT").ok().and_then(|v| v.parse().ok()).unwrap_or(3000);
Self { database_url, host, port }
}
}
5) backend/src/db.rs
use sqlx::{MySql, Pool};
use std::time::Duration;
pub async fn create_pool(database_url: &str) -> Pool<MySql> {
sqlx::mysql::MySqlPoolOptions::new()
.max_connections(20)
.min_connections(2)
.acquire_timeout(Duration::from_secs(5))
.connect(database_url)
.await
.expect("DB connect failed")
}
6) backend/src/state.rs
use sqlx::{MySql, Pool};
#[derive(Clone)]
pub struct AppState {
pub db: Pool,
}
7) backend/src/models/mod.rs
pub mod vehicle;
8) backend/src/models/vehicle.rs
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
pub struct Vehicle {
pub id: i64,
pub title: String,
pub brand: Option<String>,
pub model: Option<String>,
pub price_per_day: f64,
pub is_active: bool,
}
#[derive(Debug, Deserialize)]
pub struct CreateVehicle {
pub title: String,
pub brand: Option<String>,
pub model: Option<String>,
pub price_per_day: f64,
pub is_active: bool,
}
#[derive(Debug, Deserialize)]
pub struct UpdateVehicle {
pub title: Option<String>,
pub brand: Option<String>,
pub model: Option<String>,
pub price_per_day: Option<f64>,
pub is_active: Option<bool>,
}
9) backend/src/repositories/mod.rs
pub mod vehicle_repo;
10) backend/src/repositories/vehicle_repo.rs
use sqlx::{MySql, Pool};
use crate::models::vehicle::{Vehicle, CreateVehicle, UpdateVehicle};
pub async fn list(db: &Pool<MySql>) -> Result<Vec<Vehicle>, sqlx::Error> {
sqlx::query_as::<_, Vehicle>(
r#"
SELECT id, title, brand, model, price_per_day, is_active
FROM vehicles
ORDER BY id DESC
"#
)
.fetch_all(db)
.await
}
pub async fn get(db: &Pool<MySql>, id: i64) -> Result<Option<Vehicle>, sqlx::Error> {
sqlx::query_as::<_, Vehicle>(
r#"
SELECT id, title, brand, model, price_per_day, is_active
FROM vehicles
WHERE id = ?
LIMIT 1
"#
)
.bind(id)
.fetch_optional(db)
.await
}
pub async fn create(db: &Pool<MySql>, data: CreateVehicle) -> Result<Vehicle, sqlx::Error> {
let result = sqlx::query(
r#"
INSERT INTO vehicles (title, brand, model, price_per_day, is_active)
VALUES (?, ?, ?, ?, ?)
"#
)
.bind(&data.title)
.bind(&data.brand)
.bind(&data.model)
.bind(data.price_per_day)
.bind(data.is_active)
.execute(db)
.await?;
let id = result.last_insert_id() as i64;
// Return the created record
Ok(get(db, id).await?.expect("created row missing"))
}
pub async fn update(db: &Pool<MySql>, id: i64, data: UpdateVehicle) -> Result<Option<Vehicle>, sqlx::Error> {
// Fetch existing first
let existing = match get(db, id).await? {
Some(v) => v,
None => return Ok(None),
};
let new_title = data.title.unwrap_or(existing.title);
let new_brand = data.brand.or(existing.brand);
let new_model = data.model.or(existing.model);
let new_price = data.price_per_day.unwrap_or(existing.price_per_day);
let new_active = data.is_active.unwrap_or(existing.is_active);
sqlx::query(
r#"
UPDATE vehicles
SET title = ?, brand = ?, model = ?, price_per_day = ?, is_active = ?
WHERE id = ?
"#
)
.bind(new_title)
.bind(new_brand)
.bind(new_model)
.bind(new_price)
.bind(new_active)
.bind(id)
.execute(db)
.await?;
Ok(get(db, id).await?)
}
pub async fn delete(db: &Pool<MySql>, id: i64) -> Result<bool, sqlx::Error> {
let result = sqlx::query("DELETE FROM vehicles WHERE id = ?")
.bind(id)
.execute(db)
.await?;
Ok(result.rows_affected() > 0)
}
11) backend/src/handlers/mod.rs
pub mod vehicle_handler;
12) backend/src/handlers/vehicle_handler.rs
use axum::{
extract::{Path, State},
response::IntoResponse,
Json,
};
use tracing::error;
use crate::{
models::vehicle::{CreateVehicle, UpdateVehicle},
repositories::vehicle_repo,
state::AppState,
};
pub async fn list_vehicles(State(state): State<AppState>) -> impl IntoResponse {
match vehicle_repo::list(&state.db).await {
Ok(rows) => (axum::http::StatusCode::OK, Json(rows)).into_response(),
Err(e) => {
error!("DB error: {:?}", e);
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, Json(err_json("Server error"))).into_response()
}
}
}
pub async fn get_vehicle(State(state): State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
match vehicle_repo::get(&state.db, id).await {
Ok(Some(row)) => (axum::http::StatusCode::OK, Json(row)).into_response(),
Ok(None) => (axum::http::StatusCode::NOT_FOUND, Json(err_json("Not found"))).into_response(),
Err(e) => {
error!("DB error: {:?}", e);
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, Json(err_json("Server error"))).into_response()
}
}
}
pub async fn create_vehicle(
State(state): State<AppState>,
Json(payload): Json<CreateVehicle>,
) -> impl IntoResponse {
match vehicle_repo::create(&state.db, payload).await {
Ok(row) => (axum::http::StatusCode::CREATED, Json(row)).into_response(),
Err(e) => {
error!("DB error: {:?}", e);
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, Json(err_json("Server error"))).into_response()
}
}
}
pub async fn update_vehicle(
State(state): State<AppState>,
Path(id): Path<i64>,
Json(payload): Json<UpdateVehicle>,
) -> impl IntoResponse {
match vehicle_repo::update(&state.db, id, payload).await {
Ok(Some(row)) => (axum::http::StatusCode::OK, Json(row)).into_response(),
Ok(None) => (axum::http::StatusCode::NOT_FOUND, Json(err_json("Not found"))).into_response(),
Err(e) => {
error!("DB error: {:?}", e);
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, Json(err_json("Server error"))).into_response()
}
}
}
pub async fn delete_vehicle(
State(state): State<AppState>,
Path(id): Path<i64>,
) -> impl IntoResponse {
match vehicle_repo::delete(&state.db, id).await {
Ok(true) => (axum::http::StatusCode::OK, Json(serde_json::json!({ "deleted": true }))).into_response(),
Ok(false) => (axum::http::StatusCode::NOT_FOUND, Json(err_json("Not found"))).into_response(),
Err(e) => {
error!("DB error: {:?}", e);
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, Json(err_json("Server error"))).into_response()
}
}
}
fn err_json(msg: &str) -> serde_json::Value {
serde_json::json!({ "message": msg })
}
13) backend/src/routes/mod.rs
pub mod vehicle_routes;
14) backend/src/routes/vehicle_routes.rs
use axum::{routing::{get, post, put, delete}, Router};
use crate::{handlers::vehicle_handler, state::AppState};
pub fn routes() -> Router<AppState> {
Router::new()
.route("/api/vehicles", get(vehicle_handler::list_vehicles).post(vehicle_handler::create_vehicle))
.route("/api/vehicles/:id", get(vehicle_handler::get_vehicle).put(vehicle_handler::update_vehicle).delete(vehicle_handler::delete_vehicle))
}
15) backend/src/app.rs
use axum::{routing::get, Json, Router};
use tracing::info;
use crate::{config::Config, db::create_pool, routes, state::AppState};
pub async fn run() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into()),
)
.init();
let cfg = Config::from_env();
let db = create_pool(&cfg.database_url).await;
let state = AppState { db };
let app = Router::new()
.route("/api/health", get(health))
.merge(routes::vehicle_routes::routes())
.with_state(state);
let addr = format!("{}:{}", cfg.host, cfg.port);
info!("Rust API running at http://{}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn health() -> Json<serde_json::Value> {
Json(serde_json::json!({ "ok": true }))
}
Run backend
cd backend
cargo run
Frontend (React + Vite) — Simple CRUD UI
Folder structure
frontend/package.json
{
"name": "frontend",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.0",
"vite": "^5.4.0"
}
}
2) frontend/vite.config.js (IMPORTANT: proxy to Rust)
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": "http://127.0.0.1:3000"
}
}
});
✅ Now React can call /api/... and it goes to Rust.
3) frontend/src/main.jsx
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./app.jsx";
createRoot(document.getElementById("root")).render(<App />);
4) frontend/src/api/client.js
export async function api(url, options = {}) {
const res = await fetch(url, {
headers: { "Content-Type": "application/json", ...(options.headers || {}) },
...options
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.message || `HTTP ${res.status}`);
}
return res.status === 204 ? null : res.json();
}
5) frontend/src/api/vehicles.js
import { api } from "./client";
export const VehiclesApi = {
list: () => api("/api/vehicles"),
get: (id) => api(`/api/vehicles/${id}`),
create: (payload) => api("/api/vehicles", { method: "POST", body: JSON.stringify(payload) }),
update: (id, payload) => api(`/api/vehicles/${id}`, { method: "PUT", body: JSON.stringify(payload) }),
remove: (id) => api(`/api/vehicles/${id}`, { method: "DELETE" })
};
6) frontend/src/components/VehicleForm.jsx
import React, { useEffect, useState } from "react";
const empty = { title: "", brand: "", model: "", price_per_day: 0, is_active: true };
export default function VehicleForm({ mode, initial, onSubmit, onCancel }) {
const [form, setForm] = useState(empty);
useEffect(() => {
if (mode === "edit" && initial) {
setForm({
title: initial.title || "",
brand: initial.brand || "",
model: initial.model || "",
price_per_day: Number(initial.price_per_day || 0),
is_active: Boolean(initial.is_active)
});
} else {
setForm(empty);
}
}, [mode, initial]);
function set(k, v) {
setForm((p) => ({ ...p, [k]: v }));
}
function submit(e) {
e.preventDefault();
onSubmit({
title: form.title.trim(),
brand: form.brand.trim() || null,
model: form.model.trim() || null,
price_per_day: Number(form.price_per_day || 0),
is_active: Boolean(form.is_active)
});
}
return (
<form onSubmit={submit} style={{ border: "1px solid #ddd", padding: 12, borderRadius: 8 }}>
<h3 style={{ marginTop: 0 }}>{mode === "edit" ? "Edit Vehicle" : "Create Vehicle"}</h3>
<div style={{ display: "grid", gap: 10, gridTemplateColumns: "1fr 1fr" }}>
<label>
Title *
<input value={form.title} onChange={(e) => set("title", e.target.value)} required style={{ width: "100%", padding: 8 }} />
</label>
<label>
Price/Day *
<input type="number" value={form.price_per_day} onChange={(e) => set("price_per_day", e.target.value)} required style={{ width: "100%", padding: 8 }} />
</label>
<label>
Brand
<input value={form.brand} onChange={(e) => set("brand", e.target.value)} style={{ width: "100%", padding: 8 }} />
</label>
<label>
Model
<input value={form.model} onChange={(e) => set("model", e.target.value)} style={{ width: "100%", padding: 8 }} />
</label>
<label style={{ display: "flex", gap: 8, alignItems: "center" }}>
<input type="checkbox" checked={form.is_active} onChange={(e) => set("is_active", e.target.checked)} />
Active
</label>
</div>
<div style={{ marginTop: 12, display: "flex", gap: 8 }}>
<button type="submit" style={{ padding: "8px 12px" }}>
{mode === "edit" ? "Update" : "Create"}
</button>
{mode === "edit" && (
<button type="button" onClick={onCancel} style={{ padding: "8px 12px" }}>
Cancel
</button>
)}
</div>
</form>
);
}
7) frontend/src/components/VehicleTable.jsx
import React from "react";
export default function VehicleTable({ rows, onEdit, onDelete }) {
return (
<table width="100%" cellPadding="8" style={{ borderCollapse: "collapse", marginTop: 12 }}>
<thead>
<tr style={{ background: "#f6f6f6" }}>
<th align="left">ID</th>
<th align="left">Title</th>
<th align="left">Brand</th>
<th align="left">Model</th>
<th align="left">Price/Day</th>
<th align="left">Active</th>
<th align="left">Actions</th>
</tr>
</thead>
<tbody>
{rows.map((v) => (
<tr key={v.id} style={{ borderTop: "1px solid #eee" }}>
<td>{v.id}</td>
<td>{v.title}</td>
<td>{v.brand || "-"}</td>
<td>{v.model || "-"}</td>
<td>{v.price_per_day}</td>
<td>{v.is_active ? "Yes" : "No"}</td>
<td style={{ display: "flex", gap: 8 }}>
<button onClick={() => onEdit(v)}>Edit</button>
<button onClick={() => onDelete(v)} style={{ color: "crimson" }}>
Delete
</button>
</td>
</tr>
))}
{rows.length === 0 && (
<tr>
<td colSpan="7" style={{ padding: 16, color: "#666" }}>
No vehicles found.
</td>
</tr>
)}
</tbody>
</table>
);
}
8) frontend/src/app.jsx (CRUD page)
import React, { useEffect, useState } from "react";
import { VehiclesApi } from "./api/vehicles";
import VehicleForm from "./components/VehicleForm";
import VehicleTable from "./components/VehicleTable";
export default function App() {
const [rows, setRows] = useState([]);
const [mode, setMode] = useState("create"); // create | edit
const [editing, setEditing] = useState(null);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
async function load() {
setLoading(true);
setError("");
try {
const data = await VehiclesApi.list();
setRows(data);
} catch (e) {
setError(e.message || "Error");
} finally {
setLoading(false);
}
}
useEffect(() => {
load();
}, []);
async function onCreate(payload) {
setError("");
try {
await VehiclesApi.create(payload);
await load();
} catch (e) {
setError(e.message || "Create failed");
}
}
async function onUpdate(payload) {
if (!editing) return;
setError("");
try {
await VehiclesApi.update(editing.id, payload);
setMode("create");
setEditing(null);
await load();
} catch (e) {
setError(e.message || "Update failed");
}
}
async function onDelete(v) {
if (!confirm(`Delete vehicle #${v.id}?`)) return;
setError("");
try {
await VehiclesApi.remove(v.id);
if (editing?.id === v.id) {
setMode("create");
setEditing(null);
}
await load();
} catch (e) {
setError(e.message || "Delete failed");
}
}
return (
<div style={{ maxWidth: 980, margin: "24px auto", fontFamily: "Arial", padding: 12 }}>
<h2 style={{ marginTop: 0 }}>Vehicle CRUD (React + Rust)</h2>
{error && <div style={{ background: "#ffe8e8", padding: 10, borderRadius: 6, marginBottom: 12 }}>{error}</div>}
{loading && <div style={{ color: "#666" }}>Loading...</div>}
<VehicleForm
mode={mode}
initial={editing}
onSubmit={mode === "edit" ? onUpdate : onCreate}
onCancel={() => { setMode("create"); setEditing(null); }}
/>
<VehicleTable
rows={rows}
onEdit={(v) => { setMode("edit"); setEditing(v); }}
onDelete={onDelete}
/>
</div>
);
}
Run frontend
cd frontend
npm install
npm run dev
Laravel Blade (no change)
resources/views/vehicles.blade.php
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Vehicle CRUD</title>
@vite('resources/js/app.jsx')
</head>
<body>
<div id="app"></div>
</body>
</html>
Laravel route only to show the page (NOT API)
routes/web.php
use Illuminate\Support\Facades\Route;
Route::get('/vehicles', function () {
return view('vehicles');
});
✅ Laravel now only renders the page.
Rust will handle /api/*.
2) React (keep in Laravel resources/js)
resources/js/app.jsx
In your React API calls use:
fetch("/api/vehicles") // ✅ NOT http://127.0.0.1:3000
fetch("/api/vehicles/1")
That’s it.
3) Run Rust backend on a separate port
Rust runs like:
http://127.0.0.1:3000
Rust routes:
/api/vehicles
/api/vehicles/:id
4) IMPORTANT: Proxy /api to Rust (No CORS)
Now you choose based on your server:
A) If you use Nginx (recommended)
Inside your site config:
location /api/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
✅ Now:
https://yourdomain.com/api/vehicles → Rust
https://yourdomain.com/vehicles → Laravel Blade + React
B) If you use Apache (works well)
Enable modules:
a2enmod proxy proxy_http
In VirtualHost:
ProxyPreserveHost On
ProxyPass /api/ http://127.0.0.1:3000/api/
ProxyPassReverse /api/ http://127.0.0.1:3000/api/
✅ Same result: /api/* goes to Rust.
Top comments (0)