Working Through Rust Web Programming pt 1: Basic Server Setup

Matthew MacFarquhar
9 min readMar 23, 2024

--

Introduction

In this series, I will be working through this book on Rust Web Programming. The book is quite large and in depth, but in these articles I will be distilling the big picture pieces of the book into bite size tutorials and walk throughs.

In this section, we will talk about the Rust module setup recommended by the book and how to set up a basic web server in Rust.

The work will be tracked in this github project https://github.com/mattmacf98/web_app. The state of this tutorial is at this commit.

To Do Module

The thing we are building in this book, is a to do app (of course). The first thing we will do is create a module for to do items.

Putting the to do logic in its own separate module allows us to easily lift it out and replace it with something else, so we can use the same web framework for any other app we want to make.

Enums

The book recommends placing all enums in an enum file to encapsulate the enum functionality. If we had a lot of enums, this might be a folder with individual files, but since the only enum we have is TaskStatus, we opt for the single file.

use std::fmt;
use std::fmt::Formatter;
use serde::{Serialize, Serializer};
use crate::to_do::enums::TaskStatus::{DONE, PENDING};

#[derive(Clone)]
pub enum TaskStatus {
DONE,
PENDING
}

impl TaskStatus {
pub fn stringify(&self) -> String {
match &self {
DONE => "DONE".to_string(),
PENDING => "PENDING".to_string()
}
}

pub fn from_string(input_string: String) -> Self {
match input_string.as_str() {
"DONE" => DONE,
"PENDING" => PENDING,
_ => panic!("input {} not supported", input_string)
}
}
}

impl Serialize for TaskStatus {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer {
Ok(serializer.serialize_str(&self.stringify().as_str())?)
}
}

impl fmt::Display for TaskStatus {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match &self {
DONE => write!(f, "DONE"),
PENDING => write!(f, "PENDING")
}
}
}

Traits (Functionality)

Functionality that may be used across different items in our to do module will be kept in the traits module. This way we can write the functionality — like get — once, and allow Done and Pending tasks to both use it. Below is the get trait as an example.

use serde_json::{Map, Value};

pub trait Get {
fn get(&self, title: &String, state: &Map<String, Value>) {
let item: Option<&Value> = state.get(title);

match item {
Some(result) => {
println!("\n\nItem: {}", title);
println!("Status: {}\n\n", result);
},
None => println!("Item: {} was not found.", title)
}
}
}

Structs (Objects)

The struct folder is where we will put all our base objects. We first create a base struct which will contain shared data used by our child structs.

use serde::Serialize;
use super::super::enums::TaskStatus;

#[derive(Serialize)]
pub struct Base {
pub title: String,
pub status: TaskStatus
}

Then, our sub-structs will reference the base struct in their composition.

use crate::to_do::traits::delete::Delete;
use crate::to_do::traits::edit::Edit;
use crate::to_do::traits::get::Get;
use super::base::Base;
use super::super::enums::TaskStatus;

pub struct Done {
pub super_struct: Base
}

impl Done {
pub fn new(input_title: &str) -> Self {
let base = Base{
title: input_title.to_string(),
status: TaskStatus::DONE
};
return Done {super_struct: base}
}
}

impl Get for Done {}
impl Edit for Done {}
impl Delete for Done {}

Notice at the bottom how we impl Get, Edit and Delete for the Done object so it automatically gets the default functionality in those trait files.

Factory

The book author set up construction using the Factory pattern, which I am a huge fan of! We have a single entry point for construction, and the factory will spit out the correct struct we need for our parameters.

This logic is in the mod.rs under the todo module.

use crate::to_do::enums::TaskStatus;
use crate::to_do::structs::done::Done;
use crate::to_do::structs::pending::Pending;

pub mod enums;
pub mod structs;
pub mod traits;

pub enum ItemType {
Pending(Pending),
Done(Done)
}

pub fn to_do_factory(title: &str, status: TaskStatus) -> ItemType {
match status {
TaskStatus::DONE => ItemType::Done(Done::new(title)),
TaskStatus::PENDING => ItemType::Pending(Pending::new(title))
}
}

Interface

Now we can isolate all of our interfaces with this module into a single file (and one function). We will call it process_input and it will live inside processes.

use crate::to_do::structs::done::Done;
use crate::to_do::structs::pending::Pending;
use crate::to_do::traits::create::Create;
use crate::to_do::traits::delete::Delete;
use crate::to_do::traits::edit::Edit;
use crate::to_do::traits::get::Get;

fn process_pending(item: Pending, command: String, state: &Map<String, Value>) {
let mut state = state.clone();
match command.as_str() {
"get" => item.get(&item.super_struct.title, &state),
"create" => item.create(&item.super_struct.title, &item.super_struct.status.stringify(), &mut state),
"edit" => item.set_to_done(&item.super_struct.title, &mut state),
_ => println!("Command: {} not supported", command)
}
}

fn process_done(item: Done, command: String, state: &Map<String, Value>) {
let mut state = state.clone();
match command.as_str() {
"get" => item.get(&item.super_struct.title, &state),
"delete" => item.delete(&item.super_struct.title, &mut state),
"edit" => item.set_to_pending(&item.super_struct.title, &mut state),
_ => println!("Command: {} not supported", command)
}
}

pub fn process_input(item: ItemType, command: String, state: &Map<String, Value>) {
match item {
ItemType::Pending(pending) => process_pending(pending, command, state),
ItemType::Done(done) => process_done(done, command, state)
}
}

Summary

In summary:

  • Isolate modules into their own packages
  • Put functionality inside of a traits sub module
  • Put objects into a structs sub module
  • Create a factory for the creation of these structs
  • Create a facade function to handle interactions with this module by other modules

Web Server

Now that we have our ToDo module, we can set up our web server to interact with the module and store our ToDo items in JSON.

Setting up the Web Server

The book does a really in depth overview of async/await and how Futures work in Rust and programming in general which is a great read! But essentially he just goes over it to give you intuition about the async process used to run the actix web server behind the scenes.

use actix_cors::Cors;
use actix_web::{web, App, HttpServer, Responder, HttpRequest};
use actix_service::Service;

mod jwt;
mod to_do;
mod views;
mod state;
mod processes;
mod json_serialization;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
let cors = Cors::default().allow_any_origin().allow_any_method().allow_any_header();

App::new()
.wrap_fn(|req, srv| {
println!("{:?}", req);
let future = srv.call(req);
async {
let result = future.await?;
Ok(result)
}
})
.configure(views::views_factory).wrap(cors)
})
.bind("127.0.0.1:8000")?
.run()
.await
}

CORS let’s other hosts call our web server and get responses back (this is a very permissive CORS setup for testing and we just allow anyone to provide any method with any header).

Our web app is a very simple async function which takes in a url to determine the service (srv) to call with the request and then waits for the service to complete and returns the result.

We configure our services as views, for which we will use another factory, this time for what view should handle the request.

use actix_web::web::ServiceConfig;
use crate::views::app::app_views_factory;
use crate::views::auth::auth_view_factory;
use crate::views::to_do::to_do_view_factory;

mod auth;
mod to_do;
mod app;

pub fn views_factory(app: &mut ServiceConfig) {
auth_view_factory(app);
to_do_view_factory(app);
app_views_factory(app);
}

Now we can further split up our services into even more factories, below is the auth factory for the auth service.

mod login;
mod logout;

use actix_web::web::{ServiceConfig, get, scope};

pub fn auth_view_factory(app: &mut ServiceConfig) {
app.service(scope("v1/auth")
.route("login", get().to(login::login))
.route("logout", get().to(logout::logout))
);
}

This will tell our app that whenever we get a request to the url “v1/auth” we will enact the auth service and then we drill down into what specific view (login for “v1/auth/login” and logout for “v1/auth/logout”).

These views are very simple and just return us a string for now.

pub async fn login() -> String {
format!("Login View")
}

We will jump into more interesting views with the ToDo service.

JWT

The JWT (JSON Web Token) itself isn’t used much in this section, but it does teach us how to create this very cool functionality to add JWT anywhere a request is passed in.

For instance, in this function for editing todo

pub async fn edit(to_do_item: web::Json<ToDoItem>, token: JwToken) -> HttpResponse {
println!("here is the message in the token: {}", token.message);
...
}

We automatically can just add this parameter of type JwToken, we do not pass this token value explicitly in anywhere, it is just automatically extracted from our HTTPRequest. But How?

Well, in our JwToken class we implement FromRequest.

use actix_web::dev::Payload;
use actix_web::{Error, FromRequest, HttpRequest};
use futures::future::{Ready, ok};

pub struct JwToken {
pub message: String
}

impl FromRequest for JwToken {
type Error = Error;
type Future = Ready<Result<JwToken, Error>>;

fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
match req.headers().get("token") {
Some(data) => {
let token = JwToken{
message: data.to_str().unwrap().to_string()
};
ok(token)
},
None => {
let token = JwToken {
message: String::from("nothing found")
};
ok(token)
}
}
}
}

This will automatically try to extract the token header from our request, if it is requested by one of our views. This pattern will work for anything that we define the FromRequest trait for.

JSON State (serialize, de-serialize)

Our initial app will store state by reading and writing to a JSON file, so lets dig into how JSON serialization will work in Rust. In this project, we leaned very heavily on the Serde crate to do most of the heavy lifting for us.

Serde has a very nice Macro we can use to deserialize a given JSON into our expected object.

use serde::{Deserialize};

#[derive(Deserialize)]
pub struct ToDoItem {
pub title: String,
pub status: String
}

We can also leverage a similar macro called serialize which will take our object and turn it into JSON for us to use.

#[derive(Serialize)]
pub struct ToDoItems {
pub pending_items: Vec<Base>,
pub done_items: Vec<Base>,
pub pending_item_count: i8,
pub done_item_count: i8
}

Below is an example of how we use our Deserialization powers to take our ToDoItems and directly make an HTTPResponse with the JSON we generate.

impl Responder for ToDoItems {
type Body = BoxBody;

fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
let body = serde_json::to_string(&self).unwrap();
HttpResponse::Ok()
.content_type(ContentType::json())
.body(body)
}
}

The actual use case of the JSON is a little light in this book and the integration is slightly messy, since we do a lot of state writes in different places. Later down the road, we remove JSON state management so I am guessing it was hacked together knowing it would later be removed when we opt for a more production grade state management setup.

ToDo Views

Like the auth service we saw earlier, our todo service has its own view factory.

use actix_web::web::{ServiceConfig, post, get, scope};

mod create;
mod get;
mod edit;
mod delete;

pub fn to_do_view_factory(app: &mut ServiceConfig) {
app.service(scope("v1/item")
.route("create/{title}", post().to(create::create))
.route("get", get().to(get::get))
.route("edit", post().to(edit::edit))
.route("delete", post().to(delete::delete))
);
}

Each route will do what needs to be done for their action, utilizing the JSON state, our ToDo Factory and the process_input facade function we exposed. Below is an example of how the most complex endpoint — Edit — works.

use actix_web::{HttpRequest, HttpResponse, web};
use actix_web::web::to;
use serde_json::{Map, Value};
use crate::json_serialization::to_do_item::ToDoItem;
use crate::json_serialization::to_do_items::ToDoItems;
use crate::jwt::JwToken;
use crate::processes::process_input;
use crate::state::read_file;
use crate::to_do::enums::TaskStatus;
use crate::to_do::to_do_factory;

pub async fn edit(to_do_item: web::Json<ToDoItem>, token: JwToken) -> HttpResponse {
println!("here is the message in the token: {}", token.message);

let state: Map<String, Value> = read_file("./state.json");

let status: TaskStatus;
match &state.get(&to_do_item.title) {
None => return HttpResponse::NotFound().json(format!("{} not in state", &to_do_item.title)),
Some(result) => status = TaskStatus::from_string(result.as_str().unwrap().to_string())
}

let existing_item = to_do_factory(&to_do_item.title, status.clone());

if &status.stringify() == &TaskStatus::from_string(to_do_item.status.as_str().to_string()).stringify() {
return HttpResponse::Ok().json(ToDoItems::get_state());
}

process_input(existing_item, "edit".to_string(), &state);

return HttpResponse::Ok().json(ToDoItems::get_state());
}

We load up the current state and check to ensure that the task we are trying to edit exists.

Then, we will use the todo factory to generate our task using the values we got from the state. This piece was a little weird, it helps to think of the factory as your central location for building all your todo structs, so we are not creating a new item but just deserializing it from inputs in the state.

Lastly, after verifying the status passed in to the function is not already what the status is in the state — we don’t really need this but it helps short circuit if the state we are trying to set to is already the state of the item– we process the edit request on our item and return the updated full state to the caller.

Summary

In Summary, to build a web app we should

  • Set up an async/await Actix web app with configured services using a factory
  • Split each service into its own self contained module with a factory pointing to its end points
  • Implement FromRequest to automatically generate parameters — like JWT– from an HTTPRequest to use in our views.
  • Utilize Serde for easy JSON serialization, deserialization, writing and reading needed in our web app.

Conclusion

While this book and walkthrough build a ToDo app, the practices of modular seperation and best practices setting up a Rust web server should be extremely applicable to any project you wish to undertake. Currently, we have the web server working and we can interact with it by making requests via CURL or using Postman. In my next article, we will be building a front-end React website and an Electron Desktop app to actually interact with our server using a nice presentation layer.

--

--

Matthew MacFarquhar
Matthew MacFarquhar

Written by Matthew MacFarquhar

I am a software engineer working for Amazon living in SF/NYC.

No responses yet