Rust for the Web
Almost 3 months ago my employer accepted my idea to write our API in Rust.
The first “dev” version of the new API was deployed after 20 days — I was not only writing a new API but also learning a new programming language. The first steps were slow, the initial learning curve is steep enough. A lot, a lot of times I was near to change my decision and ask my employer to switch back to PHP, but each time Rust community was kind, friendly, and smart enough to give me advice on how to solve my issue. I’m endlessly thankful to them.
Right now 2 of our web apps (Angular 2 SPA) are published and working in “production” mode, using this REST API. These apps are very important for the business they serve.
This thing was not really difficult to understand. Of course, few first weeks I experienced a lot of errors from compiler about borrowing and lifetimes, but it’s because in other languages we don’t have such strict checks, and now I’m sure it’s a “must-have” thing.
Here is how I understand the Rust ownership system in my brains: you can pass a variable to some context either by value or by reference. In the first case be ready to say “goodbye, I will never use you in this context”. You gave a whole value — nothing left. Make a copy (clone) if you need the same value in the following lines. In the second case (reference) just be sure that source context will live longer than the target context. Otherwise, when the source context will try to die, we will have to kill value with context it belongs to, and the target context will use the reference to dead value — the compiler will not allow us to be so cruel. Same rules for returning values from a function — it’s also sending them from one context to another.
There are some more details, but these 2 rules helped me to almost forget about compiler errors, related to ownership/borrowing/lifetimes.
This ownership system removes a lot, a lot of bugs, especially dangerous things like race conditions, when code in other languages will happily write and damage your data and you will not see any warnings or failures, sometimes you will not even notice bug itself (because race conditions can be very tricky to catch). It’s very important for asynchronous algorithms also, when multiple execution flows can mutate one resource (variable, object, collection). Rust takes care of it and gives you handy tools to work with it.
The compiler is smart and gives very descriptive error messages, instead of just “no, I won’t compile it”. Sometimes you’ll even find generated examples of code to fix your errors!
Now I feel the compiler is my friend. Emotions of the first week were “stop being so pedantic, I know better it will work”. One month later I was running cargo build with words “ok, now show me where I’m wrong”. Then was a period of “damned strings”. Now I rarely see errors and almost all of them are related to things I forgot to change during refactoring — because I can refactor code without fear and I know compiler will show me where I forgot something. And I use this feature.
The most bright star here is Cargo. Cargo is awesome, I love it so much! It can help you compile your code, run tests, generate documentation, bring you crates (libraries), you will not worry about different crates versions on different machines, using the same repository. I wish we had the same tool in other languages also.
Almost all the things I tried for my web-dev needs, are covered by crates. Some of them are not as mature as I wish, but most of them are pretty usable, well documented, and with the strict compiler you can be sure — if it compiled, it will work. Frameworks Iron and Nickel are pretty active in the development and mature enough.
I’m glad to see when crates have no “unsafe” code and when they are trying to avoid usage of “panic”. I think we need some kind of badges to promote such crates.
IDE support is not perfect yet, but enough for productive work. I use the plugin for IntelliJ IDEA, it works fine and stable enough, I don’t know why they still don’t release it :) Not all kinds of autocompletion works but.. “we use beta because it’s better than nothing” :)
More information about support in IDEs can be found here: https://www.rust-lang.org/ides.html
From my point of view, the syntax is not the most strong part of Rust, but it’s the price I’m ready to pay for Rust’s features. Sometimes it’s over-verbose, especially when we talk about error handling.
After some time I get used to new keywords. Initially, I linked them with old terms I used to, and later I realized most of the new terms are new for a reason, they bring new features into language and they don’t match one-to-one with old terms. Except for “panic”. Ok, I know, it’s an arguable thing, stay calm :)
Also, two things could improve Rust: named arguments and arguments with a default value, there is an issue on github about it.
It’s static and strong, with support of generics. After PHP, I needed just a few hours to get used to strong typing and love it. I came to Rust mostly because of strong typing, way of returning results (Result/Some), awesome package manager and memory safety. Yes, I had to invest some time to learn it, to get used to it, but now, when I read my PHP code, I often wonder “Instance of what class it returns?… What does mean maybe this one?? How the hell I was able to work with it??”. Really, good things are easy to get used to.
The absolutely best part of Rust is the community. Friendly, civil, and smart people, they helped me to solve a lot of my Rust issues and explained new things to me (thank you!). Authors of crates are responsive and ready to help and listen to feedback. Rust itself evolving using RFCs, which allows the community to influent Rust evolution.
Rust has a built-in mechanism for documentation, and all the creates I tried, have documentation. Often with examples, sometimes with detailed explanations.
nginx config is pretty simple:
The most tricky part for me was hashing passwords, solved by the Scrypt module of Rust-Crypto.
For developers coming from dynamic languages, Rust compiler removes half of the reasons for tests — private functions refactoring safety (compiler gives it for free). The second half is a mix of logic errors, public API safety and remote resources communication, such as databases, files, and external APIs. Rust has builtin testing features, so you can just do it from start, without additional dependencies.
Keep calm, avoid panics, don’t use unwrap (unwrap_or is ok) and your server will be very stable. I use println and redirect output to file, to create primitive logging, later I’m going to use something more smart and reliable.
As often happens in web servers world, the only bottleneck is DB communication - in our API it takes around 98% of every request. I know Rust is a very performant language, near to C++, but I use Rust not because of it. So there will be no table with sensational numbers, sorry. You just know performance is not an issue at all in Rust, and for the web is more than enough :)
Declaration of nested structures is a pain — you can’t declare structure inside a structure, so you need to declare separately all kinds of nested structures, and they nested structures if such exists. With complex structures, it takes a lot of code to describe JSON for them. Serde even has serde_codegen to help solve this issue. For simple structures, all is pretty simple, and encoding is simple in both cases.
I use beanstalkapp and this script after files update:
This way you can just print nohup.out to read error messages.
Pros & Cons of web server in Rust
- Compilation step removes a lot of bugs so you save a lot of time because debugging is much more expensive than writing code;
- The community will help if you struggle;
- Main tools are mature enough;
- Memory safety is a big thing for long-running servers;
- thread::spawn is a very handy thing, and safe in Rust.
- The learning curve is steep. Don’t give up, ask the community when needed.
Some very simple things take more time to implement in a strongly typed language, than in dynamically typed. For example, simple database edits and changing a JSON structure before sending the response. But it’s not a big thing as I was worried initially — in the most cases benefits of shorter debug period eliminates this difference.
Learning Rust, you will not only learn new APIs but also you will learn new way of thinking, errors handling, returning errors and data control.
Good luck and welcome aboard! :)
Removed rant about Result/Some when single branch needed, thanks for this comment.
Edited information about Intellij IDEA plugin, after update from authors:
Now it should work everywhere in a single crate. You’ll need to import project instead of just opening the project folder though, and you’ll also need a very recent nightly cargo for this to work. As a bonus, import will enable Go To Symbol (Ctrl+Shift+Alt+N) for dependencies.