Rocket: Web Servers in Rust!
Welcome back to our series on building simple apps in Rust. Last week, we explored the Diesel library which gave us an ORM for database interaction. For the next few weeks, we'll be trying out the Rocket library, which makes it quick and easy to build a web server in Rust! This is comparable to the Servant library in Haskell, which we've explored before.
This week, we'll be working on the basic building blocks of using this library. The reference code for this article is available here on Github!
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
Next time, 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. Then we'll explore more advanced topics like authentication, static files, and templating!
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!