Learning Rust By Porting an Express.js Server to Rocket.rs — Part 2
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, andErr(E)
, representing error and containing an error value. Functions returnResult
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 typesResult<T, E>
andOption<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
usesstd::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 implementsFromRequest
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 animpl
block is the type we implement for, in our caseUser
, whileself
would be the actual instance the function is called with. Since we don’t implement any function which takesself
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 theDebug
trait but also theDeserialize
trait. Looking at the documentation forfrom_document
we see that it expects a type which implements theDeserialize
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'sResult<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, andForward(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.