I remember when I first tried Rust, I fell in love.
It was like a breath of fresh air after JavaScript and NodeJS.
TypeScript made the experience way better, and I believe that if you still write pure JavaScript, you are dooming yourself.
I wrote about the importance of types here and here.
But back to Rust.
My first experience with Rust—aside from trying to build my own game using ggez, bevy, and wgpu—was building a business logic layer in Rust which Electron app could call.
I wrote about my experience here and here.
This project, however, never saw light.
I’m not disappointed because I managed to craft two amazing pieces of content, and present them in one meetup and two international conferences.
But I still wanted to write a web application in Rust, because as they say: “anything that can be written in Rust, will be written in Rust”.
And on the 1st of November—I did!
I launched JustFax Online.
JustFax Online is a service that allows you to send a one-time fax without creating an account or committing to a subscription.
It’s a perfect tool for people who need to send an occasional fax.
And most important, in terms of technology, JustFax is a 100% Rust application.
The Architecture
Let’s talk about the architecture of JustFax.
It consists of two OS processes: the web server, and the dispatcher that is responsible for sending the faxes.
The web server is written in axum, which is a very popular web framework.
Axum
is built on top of tokio
, which is and asynchronous runtime for Rust; hyper
, which is a low-level HTTP implementation in Rust; and tower
, which is a collection of reusable components for building networking clients and servers.
On top of that, it uses the tera templating engine, which has a similar syntax to Jinja2.
Storage is handled by SQLite, via the rusqlite library.
The dispatcher process is just an infinite loop that polls the database for changes.
There are also some 3rd party integrations, which I won’t go into details here.
Let’s talk about my experience building a web application in Rust.
The Good
First, we will start with the good parts.
Memory usage.
This is obviously the first advantage you get from Rust.
I tweeted this image, shortly after deploying the first version.
You can find the original post on Twitter and on Mastodon.
Consider dropping a follow there as well ;)
Yes, it’s true.
A web application + a job runner, all wrapped in a supervisord
in Docker container, that takes ~30MB of RAM.
Eat that NodeJS!
Safety.
There is a saying in the Rust community that goes like this: “If it complies, it runs.”
And in general, it’s true.
You can, obviously, run into runtime issues with deserialization of 3rd-party API responses, but that’s just a side effect of building dynamic applications.
Other than this, if you are able to compile it, it will run successfully, but not necessarily correctly.
Async.
People in Rust community like to shit on async Rust.
Coming from NodeJS, where async development is heavily embedded, I can’t seem to understand the hate.
Sure, async Rust is not as mature as NodeJS.
There are different macros to make asynchronous traits, but in general, I found async Rust to be nice.
I haven’t had the chance to debug asynchronous code, which I believe can be a pain.
And you always have the option to spawn a thread, should you need to.
Type safety.
Oh, sweet Jesus!
Real type safety!
It feels incredible.
You no longer need to hunt stupid bugs where your UUID string, can have, accidentally, a non-uuid value.
A real, mature type system—is a welcoming addition to the development toolbox.
Having said that, not all is rainbows and chocolate.
The Bad
Rust is a complicated language.
I’m still far from understanding all its features.
And with such complexity, comes the downsides.
The borrow checker.
One of the biggest struggle for new Rustaceans—is the borrow checker.
In short: borrow checker is a Rust mechanism which is the heart of it’s safety and security.
This is what prevents Rust from leaking memory, or referencing a null pointer.
Understanding the borrow checker is a complicated thing.
I must admit that if you know concepts such as pass by references, pass by value, pointers, and when variables go out of scope—it helps a lot with understanding the borrow checker.
In addition to that, you barely have to touch the borrow checker by building a web application on top of axum
.
Unless you have complex data structures that hold references to other types/objects, most of the code will look, and read like a regular code in any other language.
Starting is rough.
While axum
is very similar to your favorite web framework such as RoR, fastify
, or django
—it does have unique features to it.
Things like request extractors, the way middlewares work, are error handling.
This hurts DX and the speed at which you can deliver.
I was familiar with axum
from a previous toy project I played with, but I must admit that I could have finished the project faster with NodeJS.
It could be related to the fact that I have more experience with NodeJS, or it could be a caveat of Rust development.
At that point, I no longer can answer this question reliably.
Compilation time.
On my machine, compilation is relatively fast.
However, the moment I switched to a CI/CD pipeline, I’ve encountered a relatively long compilation times.
I tweeted and tooted about the initial compilation times of ~650 seconds (which is more than 10 minutes) in a CI/CD pipeline.
I was able to reduce the compilation time to around 3 minutes, after removing the multi-stage docker build, and instead compiling the code inside the CI docker, as well as caching the dependencies.
As of today, the entire CI/CD pipeline from push to deployment takes about 6 minutes, which is not bad overall.
The ecosystem.
Rust has a very big community around it, and a great ecosystem.
The community maintains a bunch of arewe__yet websites, such as arewewebyet which list all the popular packages for a particular domain (in this case web development).
Having said that, the ecosystem is not as big as NodeJS or Python.
Many semi-popular packages and libraries—will be missing.
Most examples will be given in either JavaScript, Python, Ruby, and even PHP—but will not have Rust.
Many issues and problems you will encounter with, will require you to go through, mostly, unanswered Rust community questions; in many cases you will have to read the source code of a particular crate in order to understand how it works.
One great thing I love about the Rust community though—is that most packages are test covered, which serves as a great tool for examples.
And almost every popular package comes with a examples
folder inside the repo, where you can see popular use cases and build on top of them (example for axum
).
Conclusion
I really enjoyed building this project in Rust.
For a long time I looked for an excuse to launch a Rust project.
There is no particular reason for using Rust over, say, NodeJS.
But being and indie-hacker, I have the tendency to launch many projects without having $$ to throw on AWS or support teams.
Knowing that my application takes ~30MB of RAM, means I can run a few of them on a 1GB RAM shared VPS.
And Rust being a language that puts a lot of attention on safety, means I can run these projects without worrying of cannot read 'foo' of undefined
errors, or my entire server is crashing due to an unexpected response from a third party service.
In case you need to send a fax, consider giving JustFax Online a try.
The service is available in English, German, and French (with more languages to come).
It supports sending a fax in 50 different countries.
You don’t need to create an account or commit to a subscription model.
It takes literally 2-minutes to send a fax.
And most importantly, you get to support a fellow indie-hacker.
Follow me on Twitter and Mastodon to see more behind the scenes of my journey, as well as software engineering content.
Until next time!