Daryl Thayil
Software Engineer
Strategic Partnerships
I’ve been a full stack software engineer for about 6 years. I like to say I am full stack shaded frontend. I love solving real world problems with code. Really enjoying javascript / node, react, and scala right now but I am always open to learning new things.

Creating a basic web service in Rust: Part 2

In part 1, we explored what the Rust language was and how to get a simple hello world application up and running. In part 2 we will set up a webservice to return data so we can better understand Rust and the ecosystem around it. Part 2 assumes you have installed rust on your machine, a process explained in part 1.

Cargo

Cargo is a package manager for Rust. Cargo will build our project and manage our dependencies for us. Cargo is installed by Homebrew with the Rust compiler. Cargo let’s us create ready to go projects. Since we are up to speed with the hello world project, let’s just leave it in a dark-corner of our machine and start our new project with Cargo.

# Put this somewhere on your machine like ~/dark-corner
# --bin indicates we are making an application not a library
$ cargo new rust-webapp --bin
Created binary (application) `rust-webapp` project
$ cd rust-webapp

Well that was pretty easy. The cargo new command created a git repo for us, created a Cargo.toml file for us and created a /src folder with a main.rs file.

Cargo.toml

[package]
name = "webapp"
version = "0.1.0"
authors = ["Daryl Thayil <darylt@porch.com>"]

[dependencies]

/src/main.rs

fn main() {
    println!("Hello, world!");
}

We could use cargo build, and then run the executable file like we did earlier, or we can use cargo run to do both steps in one command.

$ cargo run
   Compiling rust-webapp v0.1.0 (file:///Users/darylt/dark-corner/Rust-webapp)
    Finished debug [unoptimized + debuginfo] target(s) in 0.14 secs
     Running `target/debug/rust-webapp`
Hello, world!

Awesome! our project is ready to go.

Iron

After looking into it for about 20 seconds, it seemed like the Iron framework was the most popular web service framework one out there, so I decided to give it a shot for my webapp. Let’s install it!

Cargo.toml

[dependencies]
iron="*"
router = "*"
rustc-serialize = "*"

We just added 3 new dependencies, our framework iron, a router for our endpoints, and a serializer (rustc_serialize) to help us deal with JSON. Whenever we run cargo run or build, cargo will download and compile any defined dependencies we have not already fetched.

Copy this into your main.rs file.

/src/main.rs


extern crate iron;
extern crate router;
extern crate rustc_serialize;

use iron::prelude::*;
use iron::status;
use router::Router;
use rustc_serialize::json;
use std::io::Read;

#[derive(RustcDecodable, RustcEncodable)]
struct SomeNumbers {
    numbers: Vec<u8>
}

fn main() {

    let mut router = Router::new();

    router.get("/", hello_world, "index");
    router.get("/test", handle_get_numbers, "test");
    router.post("/test", handle_post_numbers, "test post");

    fn hello_world( _: &mut Request) -> IronResult<Response> {
        Ok(Response::with((status::Ok, "Hello Rusty World!")))
    }

    fn handle_get_numbers(_: &mut Request) -> IronResult<Response> {
        let data = SomeNumbers { numbers: vec![1, 2, 3, 4, 5] };
        let payload = json::encode(&data).unwrap();
        Ok(Response::with((status::Ok, payload)))
    }

    fn handle_post_numbers(req: &mut Request) -> IronResult<Response> {
        let mut payload = String::new();
        req.body.read_to_string(&mut payload).unwrap();
        let my_numbers: SomeNumbers = json::decode(&payload).unwrap();
        let resp = SomeNumbers {
            numbers: my_numbers
                .numbers
                .iter()
                .map(|&num| num * 2)
                .collect::<Vec<_>>()
        };
        let response = json::encode(&resp).unwrap();
        Ok(Response::with((status::Ok, response)))
    }

    Iron::new(router).http("localhost:3000").unwrap();
}

Lets break this code up and talk about it piece by piece.

extern crate iron;
extern crate router;
extern crate rustc_serialize;

use iron::prelude::*;
use iron::status;
use router::Router;
use rustc_serialize::json;
use std::io::Read;

In our first few lines we link the crates (packages) that we just included with the extern crate keywords, and then bring in the pieces of those crates that we want to use. We also include a function from the standard lib.

#[derive(RustcDecodable, RustcEncodable)]
struct SomeNumbers {
    numbers: Vec<u8>
}

Next we define a struct which we will use in our test endpoints. The struct SomeNumbers has one property numbers which is vector(array) of unsigned 8bit ints. The notation above the struct which looks like a simple comment is actually a directive to the compiler, similar to an annotation or decorator. This essentially makes our struct encodable into json or decodable from json for our endpoints.

let mut router = Router::new();

router.get("/", hello_world, "index");
router.get("/test", handle_get_numbers, "test");
router.post("/test", handle_post_numbers, "test post");

At the top of our main function we create a new Router which will let us easily define api routes, each route takes a handler and a name.

Lets start our service, look at our endpoints and talk about what they do.

// router.get("/", hello_world, "index");
fn hello_world( _: &mut Request) -> IronResult<Response> {
    Ok(Response::with((status::Ok, "Hello Rusty World!")))
}

First we have a simple hello world root endpoint, the handler takes a Request object, and returns a response of type IronResponse. In our handler we simply return a Response Ok 200 with a string “Hello Rusty World”

# After our service is running
$  curl localhost:3000/
Hello Rusty World!

We curl our endpoint and everything seems to be working as expected, great!

Next lets look at one of our endpoints that returns JSON.

// router.get("/test", handle_get_numbers, "test");
fn handle_get_numbers(_: &mut Request) -> IronResult<Response> {
    let data = SomeNumbers { numbers: vec![1, 2, 3, 4, 5] };
    let payload = json::encode(&data).unwrap();
    Ok(Response::with((status::Ok, payload)))
}

This handler does something a little more interesting than our hello world handler. We create an instance of our struct, and initialize the array(using a macro) with some numbers. Using the Rustc_serialize json encode method we turn our struct into json. This is possible because of the compiler directive we put on our struct earlier in the code.

# After our service is running
$  curl localhost:3000/test
{"numbers":[1,2,3,4,5]}

When this endpoint, we see the result we expected, which is the struct we initialized in our code.

Finally lets look at a POST request where we deserialize, perform some operation on the data, and return a response.

// router.post("/test", handle_post_numbers, "test post");
fn handle_post_numbers(req: &mut Request) -> IronResult<Response> {
    let mut payload = String::new();
    req.body.read_to_string(&mut payload).unwrap();
    let my_numbers: SomeNumbers = json::decode(&payload).unwrap();
    let resp = SomeNumbers {
        numbers: my_numbers
            .numbers
            .iter()
            .map(|&num| num * 2)
            .collect::<Vec<_>>()
    };
    let response = json::encode(&resp).unwrap();
    Ok(Response::with((status::Ok, response)))
}

In this code, we use the standard library function we pulled in to read the request body into a string, and then we decode it. Again this is possible because of the compiler directive we gave our struct earlier in the code. We then create a new instance of SomeNumbers, and use the numbers passed into our first struct * 2 as the values in our new one. This shows off some of the functional features in Rust, although I was hoping it was a little less verbose. This reminds me more of Java streams, than something like Scala or Javascript. At last we encode our new struct and pass it back in a response.

# After our service is running
$  curl curl -X POST -d '{ "numbers": [1, 2, 3, 4] }' localhost:3000/test
{"numbers":[2,4,6,8]}

Awesome, and again things work as we expect.

Success!

Conclusion

Wow, I went from knowing not very much about Rust, to knowing even less about Rust. I tried to learn things as I needed to use them, but there is so much to dig into here that I barely scratched the surface. I hope this was as interesting for you as it was for me, please feel free to share any experiences or resources you may have.

My Impressions

  • Rust is mentally stimulating
  • Rust seems pretty lightweight
  • Rust is a lot lower level than I am used to working with, so there are a lot of concepts I need to learn / brush up on to truly take advantage of all of Rust’s features
  • The ecosystem seems good, I was was able to find packages and articles fairly easily
  • Rust’s compiler seemed really good at pointing things out to me, and even warning me about best practices

Other things I learned I didn’t touch on

  • Rust is memory safe, and protects you from issues you can get yourself into in languages like C
  • Rust has good support for using C libs
  • Features such as move and borrowing make it powerful and safe
  • Rust is functional and immutable by default
  • rustc –explain [errorcode] will describe compile errors in detail along with providing examples
  • Macros aren’t just magic, there are very real tradeoffs that should be weighed when using them

Resources