Rust with axum
Cover Page
Back-end Page
Cargo.toml: add dependencies
Change to your chatterd directory and edit the file Cargo.toml:
server$ cd ~/reactive/chatterd
server$ vi Cargo.toml
In Cargo.toml, add the following lines below the existing [dependencies] tag:
axum-extra = { version = "0.12.2" }
bb8 = "0.9.1"
bb8-postgres = "0.9.0"
chrono = { version = "0.4.42", features = ["serde"] }
postgres = { version = "0.19.12", features = ["with-chrono-0_4", "with-uuid-1"] }
serde = { version = "1.0.228", features = ["derive"] }
tokio-postgres = "0.7.15"
uuid = { version = "1.19.0", features = ["v4", "macro-diagnostics", "serde"] }
chatterd package
Edit src/main.rs:
server$ vi src/main.rs
and add the following use lines along with the PGPPool type alias:
use bb8::Pool;
use bb8_postgres::PostgresConnectionManager;
use tokio_postgres::NoTls;
type PGPool = Pool<PostgresConnectionManager<NoTls>>;
Add a pgppool property to your AppState struct definition after the line
client: Client:
pgpool: PGPool,
In the main() function, after the line, client: Client::new(), add a pgpool property to your AppState
instantiation. For the property, we set up a pool of open connections to our PostgreSQL chatterdb database.
Maintaining a pool of open connections avoids the cost of opening and closing a connection on every database
operation. The password used in instantiating PostgresConnectionManager must match the one you used when
setting up PostgreSQL earlier.
pgpool: Pool::builder()
.build(
PostgresConnectionManager::new_from_stringlike(
"host=localhost user=chatter password=chattchatt dbname=chatterdb",
NoTls,
)
.map_err(|err| {
eprintln!("{:?}", err);
process::exit(1)
})
.unwrap(),
)
.await
.map_err(|err| {
eprintln!("{:?}", err);
process::exit(1)
})
.unwrap(),
Continuing inside the main() function, find your routing table and add two routes right after the routes
for /llmprompt. These serve Chatter’s two APIs: HTTP GET request, with URL endpoint getchatts, and HTTP
POST request, with URL endpoint postchatt. With each endpoint, we also specify the MethodRouter and handler
function assigned to it:
.route("/getchatts", get(handlers::getchatts))
.route("/postchatt", post(handlers::postchatt))
The functions getchatts() and postchatt() will be implemented in handlers.rs.
We’re done with main.rs. Save and exit the file.
handlers module
Edit src/handlers.rs:
server$ vi src/handlers.rs
First add these mew use imports at the top of the file:
use chrono::{ DateTime, Local };
use serde::{ Deserialize, Serialize };
use uuid::Uuid;
We define a Chatt struct to help postchatt() deserialize JSON received from clients.
Add these lines right below the above:
#[derive(Debug, Serialize, Deserialize)]
pub struct Chatt {
name: String,
message: String,
id: Option<String>,
timestamp: Option<DateTime<Local>>,
}
To the existing logging functions, add the following:
fn logClientErr(clientIP: &SocketAddr, errcode: StatusCode, errmsg: String) -> (StatusCode, String) {
tracing::info!("{:?} | {:?} |", errcode, clientIP);
(errcode, errmsg)
}
The handler getchatts() uses an open connection from the connection pool passed in from main(),
as part of AppState, to query the database for stored chatts. Once all the rows from the database
are retrieved, we insert the resulting array of chatts into the response JSON object.
Add getchatts() to your handlers.rs after the definition of the llmprompt() function.
pub async fn getchatts(
State(appState): State<AppState>,
ConnectInfo(clientIP): ConnectInfo<SocketAddr>,
) -> Result<Json<Value>, (StatusCode, String)> {
let chatterDB = appState
.pgpool
.get()
.await
.map_err(|err| logServerErr(&clientIP, err.to_string()))?;
let mut chattArr: Vec<Vec<Option<String>>> = Vec::new();
for row in chatterDB
.query(
"SELECT name, message, id, time FROM chatts ORDER BY time ASC",
&[],
)
.await
.map_err(|err| logServerErr(&clientIP, err.to_string()))?
{
chattArr.push(vec![
row.get(0),
row.get(1),
Some(row.get::<usize, Uuid>(2).to_string()),
Some(row.get::<usize, DateTime<Local>>(3).to_string()),
]);
}
logOk(&clientIP);
Ok(Json(json!(chattArr)))
}
Similarly, postchatt() receives a posted chatt in the expected JSON format,
deserializes it into the Chatt struct, and inserts it into the database, using
a connection from the pool. The UUID and time stamp of each chatt are generated
at insertion time. Add postchatt() to the end of your handlers.rs:
pub async fn postchatt(
State(appState): State<AppState>,
ConnectInfo(clientIP): ConnectInfo<SocketAddr>,
Json(chatt): Json<Chatt>,
) -> Result<Json<Value>, (StatusCode, String)> {
let chatterDB = appState
.pgpool
.get()
.await
.map_err(|err| logServerErr(&clientIP, err.to_string()))?;
chatterDB
.execute(
"INSERT INTO chatts (name, message, id) VALUES ($1, $2, gen_random_uuid())",
&[&chatt.name, &chatt.message],
)
.await
.map_err(|err| logClientErr(&clientIP, StatusCode::NOT_ACCEPTABLE, err.to_string()))?;
logOk(&clientIP);
Ok(Json(json!({})))
}
We’re done with handlers.rs. Save and exit the file.
Build and test run
To build your server:
server$ cargo build --release
As before, it will take some time to download and build all the 3rd-party crates. Be patient.
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
| Prepared by Sugih Jamin | Last updated December 23rd, 2025 |