| name | openui-forge-rust |
| description | OpenUI generative UI with Rust Axum backend. Async SSE streaming with reqwest and async-stream. |
| version | 1.2.0 |
| author | OthmanAdi |
OpenUI Forge — Rust
Build generative UI apps with a React frontend + Rust Axum backend. Async SSE streaming to OpenAI-compatible NDJSON.
Activation Triggers
- "openui rust", "openui axum", "openui rust backend"
- "generative ui rust", "rust streaming ui backend"
Prerequisites
- Node.js >= 22 (24 LTS recommended) + React >= 18.3.1 (19+ recommended) (frontend)
- Rust >= 1.75 with Cargo (backend)
OPENAI_API_KEY environment variable set
Quick Start
- Create the React frontend and install OpenUI deps:
npm install @openuidev/react-ui @openuidev/react-headless @openuidev/react-lang lucide-react zod
- Generate the system prompt:
npx @openuidev/cli generate ./src/lib/library.ts --out backend/system-prompt.txt
- Create the Rust backend (see Full Code below)
- Run:
cargo run on :3001, frontend on :3000
Full Code
Backend: backend/Cargo.toml
[package]
name = "openui-backend"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.8"
http = "1"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.13", features = ["json", "stream"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
async-stream = "0.3"
futures = "0.3"
tower-http = { version = "0.6", features = ["cors"] }
dotenvy = "0.15"
Backend: backend/src/main.rs
use axum::{
extract::{Json, State},
response::sse::{Event, Sse},
routing::post,
Router,
};
use futures::stream::Stream;
use http::HeaderValue;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::{convert::Infallible, fs, net::SocketAddr, sync::Arc};
use tower_http::cors::{Any, CorsLayer};
#[derive(Deserialize)]
struct ChatRequest {
messages: Vec<Message>,
}
#[derive(Serialize, Deserialize, Clone)]
struct Message {
role: String,
content: String,
}
#[derive(Clone)]
struct AppState {
system_prompt: String,
}
#[tokio::main]
async fn main() {
dotenvy::dotenv().ok();
let system_prompt = fs::read_to_string("system-prompt.txt")
.expect("system-prompt.txt not found");
let state = Arc::new(AppState { system_prompt });
let cors = CorsLayer::new()
.allow_origin("http://localhost:3000".parse::<HeaderValue>().unwrap())
.allow_methods([http::Method::POST])
.allow_headers(Any);
= Router::()
.(, (chat_handler))
.(cors)
.(state);
= SocketAddr::(([, , , ], ));
();
= tokio::net::TcpListener::(addr)..();
axum::(listener, app)..();
}
(
(state): State<Arc<AppState>>,
(req): Json<ChatRequest>,
) Sse< <Item = <Event, Infallible>>> {
= [Message { role: .(), content: state.system_prompt.() }];
messages.(req.messages);
= std::env::().();
= Client::();
= async_stream::stream! {
= client
.()
.(&api_key)
.(&serde_json::json!({
: std::env::().(|_| .()),
: ,
: messages,
}))
.()
.;
(resp) = resp {
= resp.();
futures::StreamExt;
= ::();
((chunk)) = bytes_stream.(). {
buffer.(&::(&chunk));
(pos) = buffer.() {
= buffer[..pos].();
buffer = buffer[pos + ..].();
line.() {
(Event::().(&line[..]));
}
}
}
}
};
Sse::(stream)
}
Frontend: app/chat/page.tsx
"use client";
import { FullScreen } from "@openuidev/react-ui";
import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib";
import {
openAIAdapter,
openAIMessageFormat,
} from "@openuidev/react-headless";
export default function ChatPage() {
return (
<FullScreen
componentLibrary={openuiChatLibrary}
streamProtocol={openAIAdapter()}
messageFormat={openAIMessageFormat}
apiUrl="http://localhost:3001/api/chat"
/>
);
}
The Rust backend re-emits SSE via Axum's Sse<...> response (Axum wraps each Event::default().data(...) with the data: prefix). Pair it with openAIAdapter() on the frontend. openAIReadableStreamAdapter() is for NDJSON and will silently produce no output here.
System Prompt Generation
npx @openuidev/cli generate ./src/lib/library.ts --out backend/system-prompt.txt
Validation Checklist
Error Patterns
| Error | Cause | Fix |
|---|
system-prompt.txt not found | File missing | Run CLI generate command |
| CORS blocked | Origin not in CorsLayer | Update .allow_origin() |
| Partial SSE events | Chunk boundary splitting | Buffer and split on \n\n boundaries (handled in code) |
Compile error on http::HeaderValue | Missing http crate | Add http = "1" to Cargo.toml (already in the example) or import from axum::http |
static_mut_refs warning on static mut | Rust 2024 deprecates raw static mut access | Use OnceLock<String> or carry state in State<Arc<AppState>> (this example uses the latter) |
| Connection reset | Tokio runtime panic | Check .await on all async calls, verify features = ["full"] |