Rocket: Making a Web Server in Rust

Welcome to part 3 of our Rust Web series!. In part 2, we explored the Diesel library which gave us an ORM for database interaction. For the reset of the series, we'll be trying out the Rocket library, which makes it quick and easy to build a web server in Rust! This is sort of comparable to the Servant library in Haskell, which we've explored before.

In this part, we'll be working on the basic building blocks of using this library. The reference code is available here on Github! If you know the basics of Rocket already and want to try some more advanced tricks, head to part 4 where we'll combine our web server and database integration!

Rust combines some of the neat functional ideas of Haskell with some more recognizable syntax from C++. To learn more of the basics, take a look at our Rust Beginners Series!

Our First Route

To begin, let's make a simple "hello world" endpoint for our server. We don't specify a full API definition all at once like we do with Servant. But we do use a special macro before the endpoint function. This macro describes the route's method and its path.

#[get("/hello")]
fn index() -> String {
  String::from("Hello, world!")
}

So our macro tells us this is a "GET" endpoint and that the path is /hello. Then our function specifies a String as the return value. We can, of course, have different types of return values, which we'll explore those more as the series goes on.

Launching Our Server

Now this endpoint is useless until we can actually run and launch our server. To do this, we start by creating an object of type Rocket with the ignite() function.

fn main() {
  let server: Rocket = rocket::ignite();
  ...
}

We can then modify our server by "mounting" the routes we want. The mount function takes a base URL path and a list of routes, as generated by the routes macro. This function returns us a modified server:

fn main() {
  let server: Rocket = rocket::ignite();
  let server2: Rocket = server.mount("/", routes![index]);
}

Rather than create multiple server objects, we'll just compose these different functions. Then to launch our server, we use launch on the final object!

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

And now our server will respond when we ping it at localhost:8000/hello! We could, of course, use a different base path. We could even assign different routes to different bases!

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

Now it will respond at /api/hello.

Query Parameters

Naturally, most endpoints need inputs to be useful. There are a few different ways we can do this. The first is to use path components. In Servant, we call these CaptureParams. With Rocket, we'll format our URL to have brackets around the variables we want to capture. Then we can assigned them with a basic type in our endpoint function:

#[get("/math/<name>")]
fn hello(name: &RawStr) -> String {
  format!("Hello, {}!", name.as_str())
}

We can use any type that satisfies the FromParam trait, including a RawStr. This is a Rocket specific type wrapping string-like data in a raw format. With these strings, we might want to apply some sanitization processes on our data. We can also use basic numeric types, like i32.

#[get("/math/<first>/<second>")]
fn add(first: i32, second: i32) -> String {
  String::from(format!("{}", first + second))
}

This endpoint will now return "11" when we ping /math/5/6.

We can also use "query parameters", which all go at the end of the URL. These need the FromFormValue trait, rather than FromParam. But once again, RawStr and basic numbers work fine.

#[get("/math?<first>&<second>)]
fn multiply(first: i32, second: i32) {
  String::from(format!("{}", first * second)
}

Now we'll get "30" when we ping /math?5&6.

Post Requests

The last major input type we'll deal with is post request data. Suppose we have a basic user type:

struct User {
  name: String,
  email: String,
  age: i32
}

We'll want to derive various classes for it so we can use it within endpoints. From the Rust "Serde" library we'll want Deserialize and Serialize so we can make JSON elements out of it. Then we'll also want FromForm to use it as post request data.

#[derive(FromForm, Deserialize, Serialize)]
struct User {
  ...
}

Now we can make our endpoint, but we'll have to specify the "format" as JSON and the "data" as using our "user" type.

#[post("/users/create", format="json", data="<user>")]
fn create_user(user: Json<User>) -> String {
  ...
}

We need to provide the Json wrapper for our input type, but we can use it as though it's a normal User. For now, we'll just return a string echoing the user's information back to us. Don't forget to add each new endpoint to the routes macro in your server definition!

#[post("/users/create", format="json", data="<user>")]
fn create_user(user: Json<User>) -> String {
  String::from(format!(
    "Created user: {} {} {}", user.name, user.email, user.age))
}

Conclusion

In part 4 of this series, we'll explore making a more systematic CRUD server. We'll add database integration and see some other tricks for serializing data and maintaining state.

If you're going to be building a web application in Rust, you'd better have a solid foundation! Watch our Rust Video Tutorial to get an in-depth introduction!