Wicked Fast Web Servers in Rust

An introduction to writing a simple web server using Thruster

Introduction (Feel free to skip this and get to the good stuff)

There are two things I love in this world; dogs, and beautiful code. Oh… and my significant other… but she doesn’t read articles about Rust and performant web server code, so she’s not making the list for these purposes (just kidding honey! If you do read this don’t be mad!) Rust is beautiful and performant. Recently a friend and I had a conversation in which we agreed having memory management as part of the syntax of a language is the right way to do memory management. I write code for the web every day, and as such, I naturally wanted to find a web server that I could write some beautiful code in Rust with. There are a few options out there, Actix, Rocket, but none of them are quite as simple and succinct as KoaJS for node. As such, I wanted to write something as simple as Koa and as performant as a Rust framework, and I did. With that, I give you Thruster, an elegant, performant, web server written in Rust.


The Good Stuff — Building a Simple App

Thruster’s basic concept is that of middleware. Thruster’s middleware functions are a simple first in last out system that allows you to cleanly handle modifying, tracking, and responding to requests. Let’s start by making a dead simple Thruster app. If you’re new to Rust, don’t worry about it, make sure rust is installed first, then make a new folder for your project and create a new cargo file like so:

Now, let’s add Thruster and its dependencies, open up the newly generated Cargo.toml and make it look (somewhat) like this:

Great, now you have dependencies! Let’s write some actual code. The most basic application you could write in Thruster looks like the following. It has a single endpoint that returns “Hello, world!” like all good examples do.

The first piece, the extern crate lines, just the compiler what packages (called crates in Rust,) to bring into our compilation process. The next to “use” lines tell what we’re actually using.

Now to the good stuff, the first code we run into is generate_context. A context is a piece of information that’s passed between all of the middleware a request passes through. It’s how middleware can gather information and make collective changes to a particular request. generate_context is the function that gets called when a request first comes in, it creates the initial context for a request before any middleware gets fired.

Our generate_context is simple, it makes a BasicContext, a simple context that’s provided for you for convenience by the Thruster library, and assigns the body to an empty string and clones any params and query_params. In this case, params refers to pieces of information in the url such as /customers/1234/messages might have a route that looks like /customer/:customerId/messages, in which case customerId would show up in params. query_params simply refers to the search/query parameters in the url.

The next piece we’ll look at is the hello function. This is a typical piece of middleware. All middleware has two arguments, a context and a MiddlewareChain. MiddlewareChain is a data structure that handles what the next piece of middleware is in the chain. This is where the true power of Thruster comes from, and where you can tell that Thruster is largely inspired by Koa. Say we want to add a profiler to each call, for example. The profiler would:

  • Start a timer
  • Call next, wait for it to complete
  • Print out the elapsed time
  • Return, allowing any middleware that happened before it to continue

Looking at the code in the simple example, all we’re doing is changing the body of the context, and then we’re returning a weird line,

Box::new(future::ok(context))

What in the world is going on here? Box::new is a way of passing a reference to a piece of information that’s held on Rust’s heap. Does this matter? Not really, just know that in order to return a future in Thruster, you should wrap it in a Box (although with recent impl return values, that’s likely to change!)

The final piece of the puzzle is the entry point of the program, the illustrious main. Here we: create a thruster App, passing it the context generator we defined earlier so it knows how to make new contexts. The next line says “For each get request to /hello, run the following middleware functions in order. The final piece actually kicks off the app and binds it to localhost on port 4321.

That’s the simplest Thruster server! It runs very quickly, the code is simple, and it feels like Koa. Other features not covered into here include:

  • Composeable subapps: each Thruster app can be added to a parent subapp at any route.
  • Multithreading: each Thruster request is put into the thread pool and does not block other requests.
  • Reading and rendering templates: With the help of fuel_line, templates are compiled and checked during compilation, making them extremely performant.

I’ll likely cover each one of these topics in the future if there’s enough interest! Otherwise me and the dogs will keep on writing and improving Thruster for a better internet. To the moon friends. To the moon.