The Web Server and Routing
We looked at Actix Web, Tokio Axum and Rocket. Axum was chosen as it's very actively maintained and has the fastest incremental build times.
Most rust web server projects operate in a similar way. That is you configure a route and a function that will respond to that route.
The functions that respond to routes can have parameters. These parameters which might be structs
, database pools or form data are passed to the function by the framework.
Handling Configuration
We'll separate our configuration into it's own file. create crates/web-server/src/config.rs
#[derive(Clone, Debug)]
pub struct Config {
pub database_url: String,
}
impl Config {
pub fn new() -> Config {
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL not set");
Config {
database_url,
}
}
}
Handling Errors
Now is a good time to think about how we will handle errors so we don't have to unwrap
all the time.
Create a file called crates/web-server/src/errors.rs
and add the following code.
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use std::fmt;
use db::{TokioPostgresError, PoolError};
#[derive(Debug)]
pub enum CustomError {
FaultySetup(String),
Database(String),
}
impl fmt::Display for CustomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
CustomError::FaultySetup(ref cause) => write!(f, "Setup Error: {}", cause),
CustomError::Database(ref cause) => {
write!(f, "Database Error: {}", cause)
}
}
}
}
impl IntoResponse for CustomError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
CustomError::Database(message) => (StatusCode::UNPROCESSABLE_ENTITY, message),
CustomError::FaultySetup(message) => (StatusCode::UNPROCESSABLE_ENTITY, message),
};
format!("status = {}, message = {}", status, error_message).into_response()
}
}
impl From<axum::http::uri::InvalidUri> for CustomError {
fn from(err: axum::http::uri::InvalidUri) -> CustomError {
CustomError::FaultySetup(err.to_string())
}
}
impl From<TokioPostgresError> for CustomError {
fn from(err: TokioPostgresError) -> CustomError {
CustomError::Database(err.to_string())
}
}
impl From<PoolError> for CustomError {
fn from(err: PoolError) -> CustomError {
CustomError::Database(err.to_string())
}
}
Install Axum
Make sure you're in the crates/web-server
folder and add Axum to your Cargo.toml
using the following command.
cargo add [email protected] --no-default-features -F json,http1,tokio
cargo add [email protected] --F form
cargo add tokio@1 --no-default-features -F macros,rt-multi-thread
cargo add [email protected] --no-default-features
cargo add [email protected]
cargo add serde@1 -F "derive"
cargo add --path ../db
And replace your crates/web-server/src/main.rs
with the following
mod config;
mod errors;
mod root;
use std::net::SocketAddr;
use axum::{routing::get, Extension, Router};
use tower_livereload::LiveReloadLayer;
#[tokio::main]
async fn main() {
let config = config::Config::new();
let pool = db::create_pool(&config.database_url);
let app = Router::new()
.route("/", get(root::loader))
.layer(LiveReloadLayer::new())
.layer(Extension(config))
.layer(Extension(pool.clone()));
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
println!("listening on {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
Loaders
create a file called root.rs
. This is where we will have the logic that will load data from the database and pass it to the pages so they can render.
In this example we will just return JSON for now.
use crate::errors::CustomError;
use axum::{Extension, Json};
use db::User;
pub async fn loader(Extension(pool): Extension<db::Pool>) -> Result<Json<Vec<User>>, CustomError> {
let client = pool.get().await?;
let users = db::queries::users::get_users().bind(&client).all().await?;
Ok(Json(users))
}
Watching for changes and Hot Reload
We could use cargo run
to start our server but ideally we'd like our server to re-start when we make changes and for the browser to reload itself.
We've installed Just which is a command runner.
Issue the following command to create a justfile with an entry to run our server.
echo -e 'watch:\n mold -run cargo watch --workdir /workspace/ -w crates/web-server -w crates/db --no-gitignore -x "run --bin web-server"' > Justfile
You should get a Justfile
like the following.
watch:
mold -run cargo watch --workdir /workspace/ -w crates/web-server -w crates/db --no-gitignore -x "run --bin web-server"
Run the server
just watch
The server will run up. It's setup for hot reload so when you change the code, the browser will automatically update.
It also uses a very fast linker called Mold to speed up our incremental build times.
And you should be able to point your browser at http://localhost:3000
and see the web server deliver a plain text list of users.