Cover Page

Backend Page

Rust with axum

Cargo.toml: add dependencies

Change to your chatterd folder and edit the file Cargo.toml to add the 3rd-party libraries we will be using.

server$ cd ~/reactive/chatterd 
server$ vi Cargo.toml

In Cargo.toml, add the following lines below the existing [dependencies] tag:

axum-extra = { version = "0.10.1" }
bb8 = "0.9.0"
bb8-postgres = "0.9.0"
chrono = { version = "0.4.40", features = ["serde"] }
postgres = { version = "0.19.10", features = ["with-chrono-0_4", "with-uuid-1"] }
serde = { version = "1.0.219", features = ["derive"] }
tokio-postgres = "0.7.13"
uuid = { version = "1.16.0", features = ["v4", "macro-diagnostics", "serde"] }

chatterd package

Edit src/main.rs:

server$ vi src/main.rs

and replace the routing line in the use axum block with:

    routing::{ get, post },

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 the router = Router::new() instantiation statement and add two routes right after the route 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 modify the use imports at the top of the file:

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 {
    username: 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 username, message, id, time FROM chatts",
            &[],
        )
        .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 (username, message, id) VALUES ($1, $2, gen_random_uuid())",
            &[&chatt.username, &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.


:point_right: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 July 23rd, 2025