Cover Page
Backend Page
Rust with axum
Install Rust
ssh
to your server and install Rust:
server$ sudo apt install gcc # cargo depends on gcc's linker
server$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
server$ rustup update
If you see:
rustup: Command not found.
try to logout from your server and ssh back to it again. If the problem persisted and you need help updating your PATH
shell environment variable, please see a teaching staff.
The command rustup update
is also how you can subsequently update your installation of Rust to a new version.
Cargo.toml: add dependencies
First create and change into a directory where you want to keep your chatterd
package:
server$ cd ~/reactive
server$ cargo new chatterd
# output:
Created binary (application) `chatterd` package
This will create the ~/reactive/chatterd/
directory for you. Change to this directory and edit the file Cargo.toml
to list all the 3rd-party libraries (crates
in Rust-speak) we will be using.
server$ cd chatterd
server$ vi Cargo.toml
In Cargo.toml
, add the following below the [dependencies]
tag:
axum = { version = "0.8.4", features = ["macros"] }
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
reqwest = { version = "0.12.20", features = ["json"] }
serde_json = "1.0.140"
tokio = { version = "1.45.1", features = ["full"] }
tower-http = { version = "0.6.2", features = ["normalize-path", "trace"] }
tower-layer = "0.3.3"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
chatterd package
In ~/reactive/chatterd/src/
the file main.rs
has also been created for you. Edit the file:
server$ vi src/main.rs
and replace the existing lines in main.rs
with the server and URL routing code, starting with the following use
lines::
#![allow(non_snake_case)]
use axum::{
extract::{ FromRef, Request },
routing::post,
Router, ServiceExt,
};
use axum_server::tls_rustls::RustlsConfig;
use reqwest::Client;
use std::{ net::SocketAddr, process };
use tower_http::{
normalize_path::NormalizePathLayer,
trace::{ DefaultMakeSpan, DefaultOnFailure, TraceLayer },
};
use tower_layer::Layer;
use tracing::Level;
mod handlers;
After listing all our imports, we export our module handlers
, which we will define later.
We create a struct
to hold state(s) that we want to pass to each API handler. For llmprompt
, the only state we pass to its handler is an instantiation of the reqwest client, with which we will initiate a connection to the Ollama server:
#[derive(FromRef, Clone)]
pub struct AppState {
client: Client,
}
The FromRef
macro creates a “getter” for each of the struct’s property that
allows the property to be passed as axum’s State
.
Create the following main()
function and enable logging (tracing
):
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_level(false)
.with_target(false)
.compact()
.init();
And instantiate an AppState
that we will pass to each API handler:
// Create HTTP client for proxying
let appState = AppState {
client: Client::new(),
};
Continuing inside the main()
function, we next create a Router
struct to hold the URL routing information needed by axum_server. We define a route to serve llmprompt
’s single API: HTTP POST request with URL endpoint llmprompt
. We route this endpoint to the llmprompt()
function. With each route, we implicit specify which HTTP method is allowed for the URL endpoint by calling get()
, post()
, or other specific MethodRouter
. We enable the tracing middleware and pass the appState
instantiated above to each API handler:
let router = Router::new()
.route("/llmprompt", post(handlers::llmprompt))
.layer(
// must be after all handlers to be traced
TraceLayer::new_for_http()
.make_span_with(DefaultMakeSpan::new().level(Level::INFO))
.on_failure(DefaultOnFailure::new().level(Level::INFO)),
)
.with_state(appState);
The functions llmprompt()
will be implemented in handlers.rs
later.
For now, staying in main.rs
, in our main()
function, we set up the axum_server:
- assign it the
chatterd
certificate and key we created earlier, - bind it to the wildcard IP address (
0.0.0.0
, equivalent ofany
) and the default HTTPS port (443), and - wrap the
router
instance above with a layer to automatically reroute any URL specified with a trailing ‘/’ to one without trailing ‘/’, for examplehttps://YOUR_SERVER_IP/lllmprompt/
will be routed tohttps://YOUR_SERVER_IP/llmprompt
, - launch (
serve()
) the so-wrappedrouter
// certificate and private key used with HTTPS
let certkey = RustlsConfig::from_pem_file(
"/home/ubuntu/reactive/chatterd.crt",
"/home/ubuntu/reactive/chatterd.key",
)
.await
.map_err(|err| { eprintln!("{:?}", err); process::exit(1) })
.unwrap();
// bind HTTPS server to wildcard IP address and default port number:
let addr = SocketAddr::from(([0, 0, 0, 0], 443));
tracing::info!("chatterd on https://{}", addr);
// run the HTTPS server
axum_server::bind_rustls(addr, certkey)
.serve(
ServiceExt::<Request>::into_make_service_with_connect_info::<SocketAddr>(
NormalizePathLayer::trim_trailing_slash().layer(router),
),
)
.await
.map_err(|err| { eprintln!("{:?}", err); process::exit(1) })
.unwrap();
}
We’re done with main.rs
. Save and exit the file.
handlers module
We implement the URL path API handler module in src/handlers.rs
:
server$ vi src/handlers.rs
Start the file with the following use
imports:
#![allow(non_snake_case)]
use axum::{
extract::{ ConnectInfo, Json, State },
http::{ Response, StatusCode },
};
use serde_json::Value;
use std::{ net::SocketAddr, };
use crate::AppState;
We add a couple of logging functions to print to console results of handling each HTTP request and, in case of error, return a tuple of HTTP status code and error message to the client:
fn logOk(clientIP: SocketAddr) {
tracing::info!("{:?} | {:?} |", StatusCode::OK, clientIP);
}
fn logServerErr(clientIP: SocketAddr, errmsg: String) -> (StatusCode, String) {
tracing::info!("{:?} | {:?} |", StatusCode::INTERNAL_SERVER_ERROR, clientIP);
(StatusCode::INTERNAL_SERVER_ERROR, errmsg)
}
We next set the Ollama base URL string and specify the handler llmprompt()
, which
simply forwards user prompt from the client to Ollama’s generate
API using reqwest::CLient
and passes through Ollama’s reply NDJSON stream to the client:
pub const OLLAMA_BASE_URL: &str = "http://localhost:11434/api";
pub async fn llmprompt(
State(appState): State<AppState>,
ConnectInfo(clientIP): ConnectInfo<SocketAddr>,
Json(body): Json<Value>,
) -> Result<Response<reqwest::Body>, (StatusCode, String)> {
logOk(clientIP);
Ok(appState.client
.post(format!("{OLLAMA_BASE_URL}/generate"))
.json(&body)
.send()
.await
.map_err(|err| logServerErr(clientIP, err.to_string()))?
.into())
}
We’re done with handlers.rs
. Save and exit the file.
Build and test run
To build your server:
server$ cargo build --release
server$ ln -s target/release/chatterd chatterd
The first time around, it will take some time to download and build all the 3rd-party crates. Be patient.
Build release version?
We would normally build for development without the --release
flag, but due to the limited disk space on AWS virtual hosts, cargo build
for debug version often runs out of space. The release version at least doesn’t keep debug symbols around.
Linking error with cargo build?
When running cargo build --release
, if you see:
error: linking with cc failed: exit status: 1
note: collect2: fatal error: ld terminated with signal 9 [Killed]
below a long list of object files, try running cargo build --release
again. It usually works the second time around, when it will have less remaining linking to do. If the error persisted, please talk to the teaching staff.
Rust
is a compiled language, like C/C++ and unlike Python, which is an interpreted language. This means you must run cargo build
each and every time you made changes to your code, for the changes to show up in your executable.
To run your server:
server$ sudo ./chatterd
# Hit ^C to end the test
You can test your implementation following the instructions in the Testing Chatter
APIs section.
References
- The Rust Programming Language the standard and best intro to Rust.
- axum
- axum_server
- axum_server::tls_rustls
- axum examples
-
http::StatusCode
see the list of
Associated Constants
on the left menu. - axum::extract
- axum::Extension
- Derive Marco FromRef
- Serde JSON
- Loggin in Rust - How to Get Started
- Error Handling in Rust
Prepared by Chenglin Li, Xin Jie ‘Joyce’ Liu, Sugih Jamin | Last updated August 24th, 2025 |