How it feels and looks like to develop a web service in Rust
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;
}
The standard: Tokio (+ friends)
Frameworks on-top of tokio:
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))
}
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.
An example with a local store:
Example in repository for chapter 05 on GitHub.
[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" }⏎
cargo doc --open
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
rustup component add rustfmt
cargo fmt
struct Date {
day: Day,
month: Month,
year: Year,
}
struct Day(u8);
struct Month(u8);
struct Year(u32);
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);
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();