Back to all posts

Engineering

Full Stack Development in Rust

L

Lu 2024-03-24

Lapdev consists of a few components at different layers of the stack, and we've used Rust for all the them, including web frontend, web backend, server daemon, and an SSH/HTTP proxy. You may argue that Typescript/Javascript could have been a better choice for things like web frontend since it's the dominant language there, with lots of available libraries you can pick from. But for us, Rust has been nicer for reasons that I'll talk about below.

Rust is a System Language

When people talk about Rust, their first impression is that Rust is a system language. You'd probably be surprised to hear me say Rust is actually a very good language for full stack development. Rust indeed came out as a system language with memory safety without a garbage collector. But being a system language doesn't prevent Rust to be shining in developing "high level" applications. With its wonderful type system, and more and more crates, Rust is fit for lots of different areas of development.

The crates that Lapdev uses

The frontend uses Leptos to provide a single page web application. Signal is something we love to use as they are Copy.

The web backend is using Axum to serve the static content and the Restful API.

SeaORM is used for talking to the database. SeaORM uses sqlx under the hood which is a pure Rust implementation for different database drivers. Also, the migration bits can be written in Rust code, where normally you'd need to write the migration scripts in SQL in other ORM tools.

For the RPC part, we use tarpc. We prefer tarpc over gRPC because you can define the schema in code, rather than in a separate language such as .proto.

We're just trying to write everything in Rust, because you get all the goodies from the compiler and ecosystem, e.g., type checks at compile time, and auto completion from rust-analyzer etc.

Types that can be reused across the whole application are a paradise

All the types that need to be serialised/deserialised are put in a "common" crate so that whichever part of the system needs that type can import it, and then the compiler will make sure there can't be data inconsistently anywhere in the code. And whenever you're adding/removing a field from that struct, you know for sure the compiler can point you all the places that need to be changed, which gives the developer peace of mind, i.e. fearless refactoring.

Rust doesn't slow you down when you need to iterate fast

I often hear people say if you are starting a new project where you'd need to iterate very fast, Rust is often a bad choice because you'd waste lots of time thinking about lifetimes, and fighting with the compiler etc. It's simply not true. When you are at the very stage of trying your idea out for something, you can write quick dirty code in Rust with lots of .clone() and .unwrap(), and you'll find it not slow at all.

After you've tested your idea out, you'd normally need to rewrite your prototype. And again, this is where Rust's strength is, you can refactor your prototype Rust code with confidence. Search all the .unwrap() and swap them with proper error handling. Check the places where you used .clone() and see if it can be optimised.

It's actually better than you think after you've tried it yourself

If you know Rust already, I strongly suggest you try the full stack approach, and I'm sure you will love it. There's no doubt crates will be lacking in some areas, but it should be fun to write a new crate for something you need. The web is still dominated by Typescript/Javascript, but the experience of writing frontend in Rust is kind of good, especially for someone like me who is much more fluent in Rust than in Typescript/Javascript, and it's probably a nicer experience if you weigh in the type share between frontend and backend. After being spoiled by the Rust experience, I wouldn't want to use a language other than Rust for anything new, unless it's absolutely impossible to do in Rust (which I haven't experienced so far).