Learning Rust By Porting an Express.js Server to Rocket.rs — Part 2

Daniel Voigt
9 min readFeb 28, 2021

In Part 1 we set up our basic environment and implemented our first very basic route. In this part we are going to establish a connection to our MongoDB server, fetch a user from the database and set up a protected route. Again, this is more of a guideline which you can use to select the exact topics you need to learn in order to write a server in rust, not a fully-fledged tutorial.

Establish a MongoDB connection

Let’s start again by checking out the Express.js version.

First we import mongoose, get the connection string from our configuration, call connect and attach two callbacks. On a successful connection our open callback will be executed and our express server is started. The biggest thing to note is that we don’t need to pass the db instance to our express server instance. This is because modules are cached by NodeJS. We imported mongoose, set up a few parameters and called connect. If we now import mongoose again in a different module, NodeJS will serve us the cached and already connected instance of mongoose.

For the rust version we won’t be using mongoose but the official driver directly. This is because I couldn’t find any crate with such functionality I liked. If there is one, please let me know! In the meantime we create a few wrappers to emulate the ORM part. Starting with the connection itself:

Instead of using a module system which utilizes caching for us, we need to create a connection ourselves and pass it to our rocket instance. After importing all necessary functionality from the MongoDB crate (see part 1 on how we define dependencies in our Cargo.toml) we define our connection function. This function takes a &str, a reference to a str, and returns a Result<Database, mongodb::error::Error> . (Actually it returns a Future<Result<Database, mongodb::error::Error>> . You should be pretty familiar with the async await syntax so you can think of it like a Promise). &str is a reference in memory to a sequence of UTF-8 characters. If you know C/++ you can think of it a bit like char* . In contrast, String is heap allocated and mutable. Be aware there are a lot more intricacies here. Read up upon the types here.

Next up the Result type.

Result<T, E> is the type used for returning and propagating errors. It is an enum with the variants, Ok(T), representing success and containing a value, and Err(E), representing error and containing an error value. Functions return Result whenever errors are expected and recoverable.

Easy enough! But you might be asking why we use the Result type if we never return an Err(E). We actually do return the Err variant here :

let mut client_options = ClientOptions::parse("mongodb://127.0.0.1:27017").await?;

The question mark operator (?) unwraps valid values or returns erroneous values, propagating them to the calling function. It is a unary postfix operator that can only be applied to the types Result<T, E> and Option<T>.

In other words: ClientOptions::parse will either succeed and our client_options will contain a valid ClientOptions or it will encounter an error and return an Err(E) which we propagate to the caller directly. This is mostly how error handling in rust works. Structs representing either valid or invalid states and pattern matching on those structs to handle either outcome! As you can see above the other commonly used Structure here is Result<T>.

After setting up our connection we return Ok(db) to indicate that we did not encounter any errors and the caller can now use the connection. In our main function we await the result of our connection function and match the outcome. If the call was successful we match on Ok(db) which ‘unwraps’ the result for us automatically, so from Result::Ok<Database> to Database. The result in turn is wrapped in our own Connection type and attached to our rocket instance with manage. You can think of manage a bit like the app.locals from Express.js.

A quick note: If you follow the Rocket.rs guide you will notice the database section is structured differently to what we are doing. You won’t even see a MongoDB option in the master tree anymore. If we dig up the commit where the MongoDB option was removed we see why:

The latest version of 'mongodb' is async and implements its own internal
connection pooling.

Great! So the Database functionality of Rocket is only for pooling which we don’t need since the newest mongodb driver implements pooling itself. A quick look at the MongoDB docs confirms this:

This is the main entry point for the API. A Client is used to connect to a MongoDB cluster. By default, it will monitor the topology of the cluster, keeping track of any changes, such as servers being added or removed.

Client uses std::sync::Arc internally, so it can safely be shared across threads or async tasks.

Great! Let’s test out if we can connect to our Database. But how do we get a handle to our Connection instance?

Request Guards

Let’s meet Request Guards.

Request guards are one of Rocket’s most powerful instruments. As the name might imply, a request guard protects a handler from being called erroneously based on information contained in an incoming request. More specifically, a request guard is a type that represents an arbitrary validation policy. The validation policy is implemented through the FromRequest trait. Every type that implements FromRequest is a request guard.

We can think of our DB Connection as a Request Guard. The route is only valid if we can get a valid connection to our DB, otherwise we can’t serve it. Anything put in our ‘managed state’ with manage can be retrieved by a route with the State type guard! Let’s try it out with a new route:

Our route takes an argument conn of type State<'a, Connection> and returns a String. State implements the FromRequest trait, so rocket knows that if we hit this route it needs to hook into the FromRequest trait of State which itself will return our Connection. Next we use the if let syntax instead of the previous match to unwrap our Result and ignore errors since we only want to test if the connection can be established. If the result of list_collection_names(None) is Result::Ok<T> T is automatically unwrapped and assigned to the collection_names variable.

Adding this route to the mount argument of our rocket instance, starting Postman and firing a few requests we can see the “Hello, world!” response and our terminal prints all the names of our DB collections. Perfect!

Well nearly perfect. You might be confused about the <'a> syntax. Those are Lifetimes. One of the key features of rust. Up until now lifetimes where always elided, so you probably weren’t even aware of their existence.

I can’t possibly digress to completely teach you about lifetimes but to put it simply: Since our route is returning a Future we absolutely need to make sure that our Connection instance is valid when we use it at a later point in time. To remind you: we don’t have any garbage collection in rust, but rust still claims to be memory safe. What if our rocket() function exits, Connection goes out of scope and the memory is freed. We still have a Future which tries to access Connection! A use after free and therefore not memory safe! How do we prevent this? We tell rust that our returned Future (Remember since we define our function as async we don’t return just String but a Future<String> ) needs to live at least as long as our State. That’s how I read the notation. Lifetimes are hard, especially coming from a high level language like Typescript. But in system programming you need to be aware of such things. I firmly believe that by being forced to think about this you will become a better programmer in the long run. If you plan on going ahead with rust you need to learn about lifetimes.

ORM like User structure

So our connection works, and we can continue our journey. Let’s think about our protected route in terms of guards. We want to serve /secret/route only to users of our application. Great so the User itself is our guard! Let’s first implement a User structure.

It’s a struct with just a few fields, three functions implemented on that type ( impl User), and an additional implementation which is automatically added by rust thanks to the #[derive(Debug)] macro. This is in order to print our type with the {:?} format option, which accepts any type implementing Debug!

Seems pretty self-explanatory just a few remarks:

  • Self (capital S) in an impl block is the type we implement for, in our case User, while self would be the actual instance the function is called with. Since we don’t implement any function which takes self as an argument all of our functions are static functions.
  • doc! is a macro exported by the MongoDB crate to create BSON objects for our queries. ObjectId::with_string is a helper to create an ObjectID.
  • de::from_document::<User>(user) is a bit of magic. While JSON is such an integral part of (Type|Java)Script, other languages don’t usually come with built in support for this specific type of data structure. For BSON/JSON support we use the Serde crate. With Serde we can serialize and deserialize our rust structs without the need to write any implementations! You might have noticed that we didn’t only derive the Debug trait but also the Deserialize trait. Looking at the documentation for from_document we see that it expects a type which implements the Deserialize trait. The result is a ready to use User struct.

User guard

Finally we want the User struct to be used as a request guard. We can add this functionality by implementing the FromRequest trait.

In this scenario we expect a request containing a JWT in the x-api-key header which we extract from the passed Request. Next we need to retrieve some data from our managed state. Previously we extracted the State with a request guard, now we extract them with the guard function of the Request manually. First our Connection and then a JWT secret, I put into our state just like we did with Connection:

guard is an async function which returns an Outcome.

The Outcome<S, E, F> type is similar to the standard library's Result<S, E> type. It is an enum with three variants, each containing a value: Success(S), which represents a successful outcome, Failure(E), which represents a failing outcome, and Forward(F), which represents neither a success or failure, but instead, indicates that processing could not be handled and should instead be forwarded to whatever can handle the processing next.

We already learned about Result and to make the example a bit shorter I opted to use the unwrap function which unwraps our Result or panics if an error is encountered.

Next we decrypt our JWT using the JWT Crate. In this case our ‘claim’ is a JSON containing an object named user and an id field. The crate can automatically deserialize those claims with Serde. We just need to define them:

Then we call the find_active_id function from our User struct and return an Outcome depending on the result. If we find our user we just return it wrapped in an Outcome::Success. If we don’t find any valid user on the other hand we create and return an ApiError.

ApiError is another structure we define. This time I would like this structure to be able to be used as a response. If I encounter an error this structure should ‘know’ how to send itself as an HTTP response. With rocket, we can easily ‘derive’ this functionality for any type we like:

If any route now returns an ApiError, Rocket knows this type can be used as an HTTP response. Our status code is 400, our content type JSON and the body an object with a field named reason of type string.

Putting it all together

This was a lot to unpack so let’s try to create a route which uses our created guard.

Just as before a guard is defined as an argument to our route. In our case we expect either a User or an ApiError. We pattern match the parameter and either return a String with a hello message or we just return the Error. Remember by defining the Responder trait Rocket automatically knows how to create a response from this type.

You can check out the full source in the corresponding GitHub repository.

I would have to lie to say that writing a server with Rust is just as easy as with TypeScript, but I think we came a long way. Rust can be rough from time to time, learning about lifetimes and ownership especially, but in the end it’s very rewarding and fun to program with. I mostly write those articles to check myself if I understood what I previously learned.

If you can’t explain it simply, you don’t understand it well enough.

If you have any remarks, criticism or additions please let me know.

--

--

Daniel Voigt

Software developer, language enthusiast and Jazz musician.