DEV Community

Cover image for Rocket Tutorial 01: Basics
Davide Del Papa
Davide Del Papa

Posted on • Edited on

Rocket Tutorial 01: Basics

Photo by Jean-Philippe Delberghe on Unsplash, modified(cropped)

In this series we are going to explore how to make a Rust server using Rocket

The code for this tutorial can be found in this repository: github.com/davidedelpapa/rocket-tut

git clone https://github.com/davidedelpapa/rocket-tut.git
cd rocket-tut
git checkout tags/tut1
Enter fullscreen mode Exit fullscreen mode

Setup

As first thing we create a new project

cargo new rocket-tut
cd rocket-tut
Enter fullscreen mode Exit fullscreen mode

We need to override the default of rustup for this project and use Rust Nightly.

rustup override set nightly
Enter fullscreen mode Exit fullscreen mode

we will use cargo add from the cargo edit crate (if you do not have it, just run cargo install cargo-edit):

cargo add rocket
Enter fullscreen mode Exit fullscreen mode

And in fact the [dependencies] section of our Cargo.toml should look like the following:

[dependencies]
rocket = "0.4.5"
Enter fullscreen mode Exit fullscreen mode

Fantastic!

Let's substitute the default src/main.rs with the following:

#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use] use rocket::*;


#[get("/echo/<echo>")]
fn echo_fn(echo: String) -> String {
    format!("{}", echo)
}

fn main() {
    rocket::ignite().mount("/", routes![echo_fn]).launch();
}

Enter fullscreen mode Exit fullscreen mode

Let's quickly analyze the code:

  • we will use the rocket crate with the #[macro_use] directive.
  • we will define a function for each route we need, in this case echo_fn(). This function takes as a parameter a String.
  • we decorate each route with a directive that explains the HTTP method for the route (in this case GET), with the address of the route itself, and any parameter. Notice that the parameters are specified in <> and they must match the parameter names of the function.
  • the function returns a String with the body of the page.
  • lastly, in main(), we "ignite" the rocket engine (that is we start it), and we mount any route to it. After this, we "launch" the rocket (i.e., we start the server).

Now let's point the browser to http://localhost:8000. We will be greeted with a 404 page by Rocket, since we will not set up the root (/) route.

404 Error

Let's get to the route we set up, /echo/:

http://localhost:8000/echo/test

Our page greets us back with the same text we put after the echo/

Echo route working!

The echo route is working!

Great!

Serving static files

Let's see what else can we do now. We could make a static file server, for example.

Let's start it now with Cargo Watch (if it's not installed: cargo install cargo-watch)

cargo watch -x run
Enter fullscreen mode Exit fullscreen mode

Let's create a folder static/ and add an image file to it.

Now let's add the following to the src/main.rs:

use std::path::{Path, PathBuf};

#[macro_use] use rocket::*;
use rocket::response::NamedFile;
Enter fullscreen mode Exit fullscreen mode

We will use the standard library's functions to deal with the filesystem.
At the same time we use rocket's NamedFile response to serve a file

Next we add a new route for file serving:

#[get("/file/<file..>")]
fn fileserver(file: PathBuf) -> Option<NamedFile> {
    NamedFile::open(Path::new("static/").join(file)).ok()
}
Enter fullscreen mode Exit fullscreen mode

The route converts the parameter to a PathBuf: this ensures there's not a bug that allows to excalate the filesystem and go around inside the server fetching files.

We join the path to the Path of the static/ folder we just created.

Finally we need to update our list of routes, adding the fileserver function:

fn main() {
    rocket::ignite().mount("/", routes![echo_fn, fileserver]).launch();
}
Enter fullscreen mode Exit fullscreen mode

All easy:

Serving an image

There's even a easier way. On the command line, let's add the crate rocket-contrib:

cargo add rocket_contrib
Enter fullscreen mode Exit fullscreen mode

Now let's update the src/main.rs to the following:

#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use] use rocket::*;
use rocket_contrib::serve::StaticFiles;

#[get("/echo/<echo>")]
fn echo_fn(echo: String) -> String {
    format!("{}", echo)
}

fn main() {
    rocket::ignite()
        .mount("/", routes![echo_fn])
        .mount("/files", StaticFiles::from("static/"))
        .launch();
}
Enter fullscreen mode Exit fullscreen mode

As you can see we are using now a module from the rocket_contrib, serve::StaticFiles. This does exactly the "dirty work" of file serving.

We can see that we used as mount-point "/files" (while before it was / + file -> /file). We still fetch the files from the static/ folder (with StaticFiles::from())

Running it we can see its effectiveness.

Same file, different manner

Secure the server

If we get all de defaults in rocket_contrib we will have the fileserver, as well as support for JSON (some of it later).

However, rocket_contrib has got more interesting features, such as a security handler (a helmet similar to helmetjs), connections and pooling of databases, and two different templating engines, tera, and handlebars, together with a uuid function.

Let's protect our code with helmet! Let's update the rocket_contrib features:

cargo add rocket_contrib --features helmet
Enter fullscreen mode Exit fullscreen mode

Do not worry about the fileserver: as long as the defaults are not excluded, it will be present.

Now let's secure our code.

We import the SpaceHelmet after rocket:

use rocket_contrib::helmet::SpaceHelmet;
Enter fullscreen mode Exit fullscreen mode

The we will use it after we ignite the rocket:

fn main() {
    rocket::ignite()
        .attach(SpaceHelmet::default())
        .mount("/", routes![echo_fn])
        .mount("/files", StaticFiles::from("static/"))
        .launch();
}
Enter fullscreen mode Exit fullscreen mode

Here you can consult a list of the default headers set by helmet.

This way it's super easy to secure our headers.

Testing our server

Now before wrapping out this first tutorial, let's see how to test our server. Beware, it's Rocket easy, not rocket science!

As first thing, we factor out the creation of the Rocket instance:

We create a function rocket() which ingnites, and prepares out Rocket instance, without launching it. The function will return this Rocket instance:

fn rocket() -> rocket::Rocket {
    rocket::ignite().attach(SpaceHelmet::default())
    .mount("/", routes![echo_fn])
    .mount("/files", StaticFiles::from("static/"))
}
Enter fullscreen mode Exit fullscreen mode

Now we need to call this instance from our main():

 fn main() {
    rocket().launch();
}
Enter fullscreen mode Exit fullscreen mode

This is totally equivalent from our last configuration, except that in this way we can create our Rocket instance for testing purposes, outside of main().

Now we need to add a test at the end of the src/main.rs:

#[cfg(test)]
mod test {
    use super::rocket;
    use rocket::local::Client;
    use rocket::http::Status;

    #[test]
    fn echo_test() {
        let client = Client::new(rocket()).expect("Valid Rocket instance");
        let mut response = client.get("/echo/test_echo").dispatch();
        assert_eq!(response.status(), Status::Ok);
        assert_eq!(response.body_string(), Some("test_me".into()));
    }
}
Enter fullscreen mode Exit fullscreen mode

With super::rocket we get the rocket() function in order to init the instance

We define the test function echo_test() which as first thing sets up a client to poll the server. The first thing we test is actually that we excpect() a valid instance of Rocket!

Then we dispatch a GET request to the server, at the route echo/, sending it a "test_echo" parameter. We store the response.

We test for two things: the first is that the response status is Ok (that is 400).
The second thing is that the body of the response must match our echo parameter. In this case the code is set up to fail, since we sent "test_echo" as parameter, and we are testing against "test_me".

A quick run with cargo test should in fact return a test failure:

cargo test

....

thread 'test::echo_test' panicked at 'assertion failed: `(left == right)`
  left: `Some("test_echo")`,
 right: `Some("test_me")`', src/main.rs:33:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    test::echo_test

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

error: test failed, to rerun pass '--bin rocket-tut'
Enter fullscreen mode Exit fullscreen mode

One little adjustment to the code, assert_eq!(response.body_string(), Some("test_echo".into())); and the test runs smooth:

running 1 test
test test::echo_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Enter fullscreen mode Exit fullscreen mode

Conclusion

Ok, that is all for today.

We have introduced Rocket, its basic usage, the securing that can be done using rocket_contrib, and how to run tests.

In our next installment we will see how to create a CRUD server with it. Stay tuned!

Top comments (0)