Frontend Integration

Integrating Rust backends with frontend applications and full-stack Rust.

Advancedā±ļø 50 minšŸ“š Prerequisites: 1

Frontend Integration

Rust backends can integrate with various frontend technologies, and Rust can also be used for frontend development.

Backend + Frontend Architecture

Traditional: Rust API + JavaScript Frontend

Frontend (React/Vue/Angular)
    ↓ HTTP/REST
Rust Backend (Axum/Actix)
    ↓
Database

Full-Stack Rust

Frontend (Yew/Leptos/Dioxus)
    ↓ HTTP/WebSocket
Rust Backend (Axum/Actix)
    ↓
Database

Rust Backend for Frontend

CORS Configuration

RUST
// src/api/middleware/cors.rs
use tower_http::cors::{CorsLayer, Any, AllowOrigin};

pub fn cors_layer() -> CorsLayer {
    CorsLayer::new()
        .allow_origin(Any)
        .allow_methods(Any)
        .allow_headers(Any)
        .allow_credentials(true)
}

// In main.rs
let app = Router::new()
    .route("/api/users", get(get_users))
    .layer(cors_layer());

API Structure for Frontend

RUST
// src/api/routes.rs
pub fn create_router() -> Router {
    Router::new()
        .nest("/api/v1", api_v1_routes())
        .layer(cors_layer())
}

fn api_v1_routes() -> Router {
    Router::new()
        .route("/users", get(get_users).post(create_user))
        .route("/users/:id", get(get_user).put(update_user))
        .route("/auth/login", post(login))
        .route("/auth/register", post(register))
}

WebSocket Support

RUST
// src/api/websocket.rs
use axum::extract::ws::{WebSocket, Message};

pub async fn websocket_handler(socket: WebSocket) {
    let (mut sender, mut receiver) = socket.split();
    
    while let Some(msg) = receiver.next().await {
        match msg {
            Ok(Message::Text(text)) => {
                // Handle message from frontend
                sender.send(Message::Text("Response".to_string())).await;
            }
            Ok(Message::Close(_)) => break,
            _ => {}
        }
    }
}

// In routes
.route("/ws", get(websocket_handler))

Rust Frontend Frameworks

1. Yew (React-like)

TOML
[dependencies]
yew = "0.21"
wasm-bindgen = "0.2"
RUST
// src/lib.rs
use yew::prelude::*;

#[function_component]
fn App() -> Html {
    let counter = use_state(|| 0);
    
    let increment = {
        let counter = counter.clone();
        Callback::from(move |_| counter.set(*counter + 1))
    };
    
    html! {
        <div>
            <p>{ *counter }</p>
            <button onclick={increment}>{ "Increment" }</button>
        </div>
    }
}

#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn main() {
    yew::Renderer::<App>::new().render();
}

2. Leptos (Modern, Fast)

TOML
[dependencies]
leptos = "0.6"
leptos_axum = "0.6"
RUST
// src/lib.rs
use leptos::*;

#[component]
fn App() -> impl IntoView {
    let (count, set_count) = create_signal(0);
    
    view! {
        <div>
            <p>{count}</p>
            <button on:click=move |_| set_count.update(|n| *n += 1)>
                "Increment"
            </button>
        </div>
    }
}

3. Dioxus (Cross-platform)

TOML
[dependencies]
dioxus = "0.5"
RUST
// src/main.rs
use dioxus::prelude::*;

fn main() {
    dioxus::launch(App);
}

fn App() -> Element {
    let mut count = use_signal(|| 0);
    
    rsx! {
        div {
            p { "Count: {count}" }
            button { onclick: move |_| count += 1, "Increment" }
        }
    }
}

Full-Stack Rust Project Structure

fullstack-rust/
ā”œā”€ā”€ Cargo.toml              # Workspace
ā”œā”€ā”€ backend/
│   ā”œā”€ā”€ Cargo.toml
│   └── src/
│       ā”œā”€ā”€ main.rs
│       ā”œā”€ā”€ api/
│       └── services/
ā”œā”€ā”€ frontend/
│   ā”œā”€ā”€ Cargo.toml
│   └── src/
│       └── lib.rs
└── shared/
    ā”œā”€ā”€ Cargo.toml          # Shared types
    └── src/
        └── lib.rs

Shared Types

RUST
// shared/src/lib.rs
use serde::{Serialize, Deserialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: u64,
    pub email: String,
    pub name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateUserRequest {
    pub email: String,
    pub name: String,
}

Frontend-Backend Communication

RUST
// frontend/src/api.rs
use wasm_bindgen_futures::spawn_local;
use gloo_net::http::Request;

pub async fn fetch_users() -> Result<Vec<User>, Error> {
    let response = Request::get("http://localhost:3000/api/users")
        .send()
        .await?;
    response.json().await
}

// In component
let users = use_state(|| Vec::new());

use_effect(move || {
    spawn_local(async move {
        if let Ok(fetched_users) = fetch_users().await {
            users.set(fetched_users);
        }
    });
});

Deployment Architecture

Development

Frontend (localhost:8080)
    ↓
Backend (localhost:3000)
    ↓
Database

Production

CDN/Static Hosting (Frontend)
    ↓ HTTPS
Load Balancer
    ↓
Rust Backend (Multiple Instances)
    ↓
Database Cluster

Best Practices

  • API versioning: Version your APIs
  • Type safety: Share types between frontend/backend
  • Error handling: Consistent error responses
  • Authentication: JWT tokens, secure cookies
  • CORS: Configure properly for production
  • WebSocket: For real-time features
  • SSR: Server-side rendering for SEO (Leptos supports this)
  • Build optimization: Optimize WASM bundle size

Code Examples

Full-Stack Structure

Organizing full-stack Rust project

RUST
fn main() {
    println!("Full-stack Rust:");
    println!("- Backend: API server");
    println!("- Frontend: WebAssembly");
    println!("- Shared: Common types");
}

Explanation:

Full-stack Rust projects use a workspace with separate crates for backend, frontend, and shared code. This enables type safety across the stack.

Frontend-Backend Integration

Frontend calling Rust backend

RUST
struct User {
    id: u64,
    name: String,
}
fn main() {
    println!("Frontend-Backend integration:");
    println!("- Frontend makes HTTP requests");
    println!("- Backend returns JSON");
    println!("- Shared types ensure type safety");
}

Explanation:

Frontend and backend communicate via HTTP. Shared types ensure both sides use the same data structures, providing compile-time type safety.

Exercises

Shared Type

Create a shared User type that can be used by both frontend and backend!

Easy

Starter Code:

RUST
fn main() {
    println!("Shared type created!");
}