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 tokio@1 --no-default-features -F macros,fs,rt-multi-thread
cargo add --path ../db
And replace your crates/web-server/src/main.rs
with the following
mod config;
mod errors;
use crate::errors::CustomError;
use axum::{extract::Extension, response::Json, routing::get, Router};
use std::net::SocketAddr;
use db::User;
#[tokio::main]
async fn main() {
let config = config::Config::new();
let pool = db::create_pool(&config.database_url);
let app = Router::new()
.route("/", get(users))
.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();
}
async fn users(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))
}
Watch the Server
We could use cargo run
to start our server but Rust on Nails comes with a built in alias that will watch your code for changes and restart your server.
It also uses a very fast linker called Mold to speed up our incremental build times.
Issue the following command in your app
folder.
cw
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.