Simple web server in Rust

Are you curious about Rust and want to learn but have heard it is too difficult? Or do you want to get a feel on how things are before you start studying? This post is for you. My intention here is not to show any advanced techniques, but to prove that Rust is not a monster and that it is very similar to what you already find in other languages.

Here I will introduce and explain the code of a web service. It doesn’t have a tutorial character where I usually build the code from scratch showing the step by step. I’m going to show the code for a simple server and comment out each part. So you can compare the general structure with what you already know from other tools.

In the most basic web service structure you have the following components:

  • Server object – To listen and answer http calls.
  • Some portion of the code to map urls to handler functions.
  • The handler functions that will handle each specific call.

So let’s go to the code that cover the basics. It is a service to calculate the area of a rectangle. I suggest reading a little for yourself and trying to understand. Then we go to the comments. You can also see this code on github.

use actix_web::{web, App, HttpResponse, HttpServer};
use serde::Deserialize;

// To map url parameters to a struct.
#[derive(Deserialize)]
struct RectangleAreaParameters {
    width: u64,
    height: u64,
}

fn main() {
    // Define the server with two routes.
    let server = HttpServer::new(|| { 
        App::new()
            .route("/", web::get().to(index))
            .route("/area", web::post().to(area)) 
    });

    // Start the server.
    println!("Serving on http://localhost:3000...");
    server
        .bind("127.0.0.1:3000").expect("error binding server to address")
        .run().expect("error running server");
}

// Index Handler
fn index() -> HttpResponse {
    HttpResponse::Ok()
        .content_type("text/html")
        .body(
            r#"
                <title>Rectangle Area Calculator</title>
                <form action="/area" method="post">
                    <input type="type" name="width"/>
                    <input type="type" name="height"/>
                    <button type="submit">Calculate Area</button>
                </form>
            "#,
        )
}

// Calculate Area Handler
fn area(parameters: web::Form<RectangleAreaParameters>) -> HttpResponse {
    if parameters.width == 0 || parameters.height == 0 {
        return HttpResponse::BadRequest()
            .content_type("text/html")
            .body("If one side is 0 it's only a line.");
    }

    let response = 
        format!("The area of the rectangle with width {} and height {} \
                is <b>{}</b>", parameters.width, parameters.height, calculate_area(parameters.width, parameters.height));

    HttpResponse::Ok()
        .content_type("text/html")
        .body(response)
}

// Simple area calculation
fn calculate_area(width: u64, height: u64) -> u64 {
    return width * height;
}

Let’s understand part by part.

use actix_web::{web, App, HttpResponse, HttpServer};
use serde::Deserialize;

We are using two libs, actix_web as our server framework and serde to parse our url parameters.

#[derive(Deserialize)]
struct RectangleAreaParameters {
    width: u64,
    height: u64,
}

It is a simple way to receive url parameters. When a call in the form ‘?width=2&height=3’ the serde with actix take care of transforming this data to the struct format that you will receive in the handler area the variable parameters ready. More on area handler.

let server = HttpServer::new(|| { 
    App::new()
        .route("/", web::get().to(index))
        .route("/area", web::post().to(area)) 
});

Starts the variable that contains the server and registers two routes:

  • A GET on the / route that will call the index function.
  • A POST in the /area route that will call the area function.
println!("Serving on http://localhost:3000...");
server
    .bind("127.0.0.1:3000").expect("error binding server to address")
    .run().expect("error running server");

It tries to reserve the address “127.0.0.1:3000” for the server and then starts it.

The strangest part of these lines is the call to expect. This is how Rust handles errors. Just as Golang opted for an alternative to the classic try-catch, Rust did the same. This is a peculiarity of the language that needs to be understood (nomination at the end). At first I said that Rust isn’t a monster, but that doesn’t mean that everything is the same. And if it is, it wouldn’t even have to exist.

fn index() -> HttpResponse {
    HttpResponse::Ok()
        .content_type("text/html")
        .body(
            r#"
                <title>Rectangle Area Calculator</title>
                <form action="/area" method="post">
                    <input type="type" name="width"/>
                    <input type="type" name="height"/>
                    <button type="submit">Calculate Area</button>
                </form>
            "#,
        )
}

The ‘->’ simply indicates the return type of a function (in this case HttpResponse). Most languages don’t have a symbol for this. But once you know what it is, there’s nothing complicated.

The answer here is an OK status (200), with a contentType and Body. It’s the most basic response type you’ll get on the web, and it follows the same pattern when implemented in other languages.

r#” and “# simply indicate a string written on several lines. In other languages you find triple quotes ‘’’long string‘’’. Again, weird just until you know what it is.

fn area(parameters: web::Form<RectangleAreaParameters>) -> HttpResponse {
    if parameters.width == 0 || parameters.height == 0 {
        return HttpResponse::BadRequest()
            .content_type("text/html")
            .body("If one side is 0 it's only a line.");
    }

    let response = 
        format!("The area of the rectangle with width {} and height {} \
                is <b>{}</b>", parameters.width, parameters.height, calculate_area(parameters.width, parameters.height));

    HttpResponse::Ok()
        .content_type("text/html")
        .body(response)
}

The weirdest part is the function argument. As mentioned before, here the magic takes place. You just define the type as ‘web::Form’ and actix takes care of giving you the url parameters in an struct. Something similar happens in Golang when you use midwares or in Pyhon/Django with some annotations. It is a code the framework runs before entering the function itself.

The ‘format!’ is a macro to format text + variables into a string. Perhaps the difference here is the ‘!’ symbol which indicates that it is not a function, but a macro. Macros are common in system languages like C and C++ and are usually evaluated at compile time to generate other code. In practice, they are usually faster than a normal function by avoiding a function call on the stack, but it goes well beyond that.

fn calculate_area(width: u64, height: u64) -> u64 {
    return width * height;
}

Finally a very simple function for calculating area. u64 is the unsigned int 64 type for Rust.

If you want to download the github code and run it to test it yourself, just run the ‘cargo run’ command.

As you can see, Rust has its own syntax for some things but that doesn’t mean language is a monster or very difficult to learn. Once you understand what these slightly weird symbols are (and that they are common things on other languanges), it becomes more comfortable to understand codes in Rust. I’m not advocating however that Rust is a super simple language. In fact it is more complex than Golang or Python. But a monster? No.

There is indeed a monster in this story, our weak mind that is frightened by any comment that something is difficult or that things are impossible to learn. The truth is, you’ll never learn anything deeply, not even JavaScript, if you don’t put the time and effort on it. Getting good references is also critical to learning well.

Are you interested in learning Rust in depth, understanding everything from language basics to concurrency and security? I recommend O’Reilly’s book, Programming Rust – Fast, Safe Systems Development. Posts are good for introducing or learning specific techniques, but they are not a substitute for a good book or a good full course.

const vs mut, thinking different

Sometimes you got yourself thinking about curious things. Why is it that way? Why does it exist?

One day I found myself wondering why in Rust I need to declare my variable with mut for it to actually be variable (to change).

To put it in context in most languages there are variables and constants. See an example with Golang:

var a int = 0
a = 1
fmt.Println(a)

const b int = 0 
b = 1    // Compilation error on this line as it is not possible to change a constant.
fmt.Println(a)

With Rust things are a little different:

let mut a: u64 = 0;
a = 1;
println!("{}", a);
    
let b: u64 = 0;
b = 1;    // Compilation error on this line as it is not possible to change a constant.
println!("{}", b);

Rust also has the reserved word const but for another purpose beyond the scope of this brief reflection.

Initially you might imagine it’s just a different way of writing the same things. But there is a change in philosophy behind this small detail. In the first case the thought is:

  • Everything is variable until you say otherwise.

In Rust this changes to:

  • Everything is fixed until said otherwise.

The difference here is in the concern with code security. Over time I’ve realized that Rust forces me to think more about what I’m going to do with each variable I use. Where and how will this variable be used? Does it really need to change? And the more you think about your code, the more likely it is to get better quality.

In fact, getting a Rust code that compiles without errors and warnings is a little more work. But for details like these, in general my code has more quality when programming with Rust.

One lesson I learned in that little daydream: Don’t overlook details. Thinking about the little things also teaches you something and can make you a better professional.