The progression of a Rust web service

How it feels and looks like to develop a web service in Rust

Bastian Gruber

  • Rust developer for the past ~4 years
  • Author of "Rust Web Development"
  • Founder & Co-Organizer "Rust&Tell Berlin"
  • Former Systems Enginner @krakenfx
  • Now Solutions Architect @twilio
  • @recvonline

"Rust Web Development"

  • Pragmatic approach of Web Development in and with Rust
  • "Getting Things Done" mentality with enough theory for further exploration
  • Meant to give out to new hires, or
  • Getting ready for your new Rust job/role
  • rustwebdevelopment.com

Today's talk

  • Understand Rusts web ecosystem
  • "How does a web service in Rust look like?"
  • "Can Rust do X?"
  • How to get started (what to choose)
  • Progressing and structuring a Rust web service
  • Tools to keep a web service sound and solid
  • Helpful Rust patterns
  • Little helpers along the way
Rust ♥ Web
Rust ♥ (?) Web

Rust has no built-in HTTP support (just TCP and lower)

Without HTTP, just TCP streams are handled within the Rust std

                            
                                use std::net::{TcpListener, TcpStream}; 
                                        
                                fn handle_client(stream: TcpStream) { 
                                    // do something 
                                 } 
                                        
                                 fn main() -> std::io::Result<()> { 
                                    let listener = TcpListener::bind("127.0.0.1:80")?; 
                                    for stream in listener.incoming() { 
                                        handle_client(stream?); 
                                    } 
                                    Ok(()) 
                                 }
                            
                          

In addition, async needs extra help from crates to work.

Putting it all together...

And in code...

                            use warp::Filter;
                                
#[tokio::main]
async fn main() {
    // GET /hello/warp => 200 OK with body "Hello, warp!"
    let hello = warp::path!("hello" / String)
        .map(|name| format!("Hello, {}!", name));

    warp::serve(hello)
        .run(([127, 0, 0, 1], 3030))
        .await;
}
                            
                        
                            use warp::Filter;
                                
#[tokio::main]
async fn main() {
    // GET /hello/warp => 200 OK with body "Hello, warp!"
    let hello = warp::path!("hello" / String)
        .map(|name| format!("Hello, {}!", name));

    warp::serve(hello)
        .run(([127, 0, 0, 1], 3030))
        .await;
}
                            
                        
                            use warp::Filter;
                                
#[tokio::main]
async fn main() {
    // GET /hello/warp => 200 OK with body "Hello, warp!"
    let hello = warp::path!("hello" / String)
        .map(|name| format!("Hello, {}!", name));

    warp::serve(hello)
        .run(([127, 0, 0, 1], 3030))
        .await;
}
                            
                        
                            use warp::Filter;
                                
#[tokio::main]
async fn main() {
    // GET /hello/warp => 200 OK with body "Hello, warp!"
    let hello = warp::path!("hello" / String)
        .map(|name| format!("Hello, {}!", name));

    warp::serve(hello)
        .run(([127, 0, 0, 1], 3030))
        .await;
}
                            
                        
How to get started
Choose a web framework (+ runtime)

The standard: Tokio (+ friends)

Frameworks on-top of tokio:

  • warp
  • axum
  • actix-web

Copy+paste the "Hello, World" example and get familiar with the context.

For example, warp:

                            use warp::Filter;
                                
#[tokio::main]
async fn main() {
    // GET /hello/warp => 200 OK with body "Hello, warp!"
    let hello = warp::path!("hello" / String)
        .map(|name| format!("Hello, {}!", name));

    warp::serve(hello)
        .run(([127, 0, 0, 1], 3030))
        .await;
}
                            
                        

All frameworks have rather excellent documentation, so head over to docs.rs and get familiar with the concepts and example code.

Example: warp on docs.rs

Next step is to provide your first API endpoint(s) and create your first route handlers.

Feel free to put everything in main.rs in the beginning (first one or two routes).

For example: Code for chapter 04 of the book on GitHub.

Later, use modules to split your code apart (more on that later).

serde

#[derive(Deserialize, Serialize)]
struct Question {
    id: String,
    title: String,
    content: String,
}

Serde provides macros (Deserialize and Serialize) which offer JSON, BSON etc. support so you don't have to manually transform data structures from and to your response format like JSON.

#[derive(Deserialize, Serialize)]
struct Question {
    id: String,
    title: String,
    content: String,
}

Because of the Serialize macro on top of the Question struct, serde will automatically transform a list of questions into valid JSON.

async fn get_questions() -> Result<...> {
        ...
        Ok(warp::reply::json(&store.questions))
}
Things to look out for

Memory management

Rusts Ownership model might* influence your code and function design and architecture.


*will

Refactoring your code might end up with a larger impact on your code than in other languages.

Sharing memory between threads

You have to have in mind that you share data between threads (in an async environment) so you have to learn why and how to use.
There is an excellent tutorial from tokio.

An example with a local store:

Steps to create a solid foundation
Move parts into their own modules

Example in repository for chapter 05 on GitHub.

  • main.rs: Has fn async main in it, which starts the server and passes the requests to the route handlers.
  • store.rs: Local store or DB access (struct, traits and impl struct).
  • types/*.rs: All (business) types you need. Each file has struct, impl struct and evtl traits.
  • routes/*.rs: All route handlers (e.g. get_questions, get_answers etc.)
Use git paths for own (private) modules
[package]
name = "rust-web-development"
version = "0.1.0"
authors = ["Bastian Gruber "]
edition = "2018"

[dependencies]
warp = { git = "https://GIT_URL/PRIVATE_REPO" }
parking_lot = "0.10.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.1.1", features = ["full"] }
handle-errors = { path = "handle-errors", version = "0.1.0" }⏎  

Larger projects can be split up in "workspaces"

[package]
name = "rust-web-development"
version = "0.1.0"
authors = ["Bastian Gruber "]
edition = "2018"

[workspaces]
members = [
    "handle-errors",
    ...
]

[dependencies]
warp = { git = "https://GIT_URL/PRIVATE_REPO" }
parking_lot = "0.10.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.1.1", features = ["full"] }
handle-errors = { path = "handle-errors", version = "0.1.0" }⏎  
How to solidify the foundation
Comments and Doc-Comments
cargo doc --open
Linting (Clippy)
rustup component add clippy
cargo clippy
 warning: length comparison to zero 

  --> src/routes/question.rs:12:6 

   | 
12 |   if params.len() > 0 { 
   |      ^^^^^^^^^^^^^^^^ help: using `!is_empty` is 
            clearer and more explicit: `!params.is_empty()` 

 
cargo clippy --fix
Rust formatter: rustfmt
rustup component add rustfmt
cargo fmt
Testing: Built-in
Logging,tracing and debugging: Read the book ;)
For async web services, tracing is the recommended crate.
Rust patterns
Newtype
                            
                                struct Date {
                                    day: Day,
                                    month: Month,
                                    year: Year,
                                }
                                    
                                struct Day(u8);
                                struct Month(u8);
                                struct Year(u32);
                            
                          
Constructor
                            
                                impl Date {
                                        fn new(day: u8, month: u8, year: u32) -> Self {
                                            day: Day(day),
                                            month: Month(month),
                                            year: Year(year),
                                        }
                                }

                                let d = Date::new(1,1,2000);
                            
                          
Builder (example)
                            
                                impl Date {
                                    fn with_day(mut self, day: Day) -> Self {
                                        self.day = Day;
                                        self
                                    }

                                    fn build(self) -> Date {
                                       Date {
                                         day: self.day,
                                       }
                                    }
                                }

                                let d = Date::new().day(Day(1)).build();
                            
                          
Little helpers along the way
Rust Playground
Exercism
transform
rustwebdevelopment.com