I’ve spent a scary amount of time with #Ruby and #RubyOnRails (nearly 2 decades at this point) and given the recent developments in the community and circumstances in general vigorously gesturing at everything I though I'd explore what's out there.

I decided to try #Rust for web dev. I have to say that I dubbed my toes regularly into Rust but never used it extensively and never learned it properly. It seems I reread the Rust Book like every three years and I still can’t fully grasp it. I did Advent of Code this year with Rust. Finished all the tasks. So I felt I might try as well something else.

Anyway, I did a quick search. I got to https://www.arewewebyet.org/ — it says “Yes! And it's freaking fast!” so I was encouraged. I looked at what it recommends and off I went.

So here's a rare thread. I’m doing a series of posts because I want to vent a bit and "be wrong on the internet" in hope that someone’s gonna set the record straight and I will learn something. A thread will hopefully make it easier for the hivemind to address individual points.

PLEASE, REPLY TO THESE POSTS.

Anyway, here we go. 🧵

Are we web yet? Yes, and it's freaking fast!

AreWeWebYet gives insight on whether you can build your latest web-project on top of Rust

Are We Web Yet?

I had to start with something. I picked #Axum. It’s one of the most popular frameworks and it's actively developed. So good enough for start.

First I have to address the “framework” here. IT terms of scope Axum is closer to Sinatra than Rails. And closer from the opposite side. It's somewhere between Rack and Sinatra.

The central part of Axum is the router and API for request-response mapping into types.

So in Ruby pretty much every web framework has some sort of Request and Response objects. They completely encapsulate both concepts and you have to stick your fingers deep into Request object to get anything you may want.

You still can do that in Axum but generally your “actions” (in Rails terminology) are just functions. And arguments to those functions pieces of the request that you need to process the request. It can be a piece of the request path (like your post id from /posts/42), or cookies, or any specific header, or parsed json request body, or form data, etc. So where in Rails you get request (or at bests params) in Axum you get very specific and typed pieces of information.

Your handler may look like this:

fn create_post(Form(post_form): Form<PostForm>, cookies: CookieJar) -> Result<(), StatusCode> {
//…
}

Now, Rails docs are great. Specifically the Guides. You can learn every piece of Rails from them. There are no guides for Axum. There are API docs. They're very different to Rails docs. It will take some time learning to read the docs. But the main learning material seems to be examples. Examples are indispensable. But they're sort of terrible to learn all the possible types the handler function can take.

I had to actually go and lear the magic that makes this typed handlers witchcraft work to understand what I can plug in there. (It's explicit type coercion and a little macro.) I'm glad I understand it now but I have to point out that I spend much longer trying to figure that out reading the docs and still didn't until I read the code. It's not a good learning experience.

#Ruby #RubyOnRails #Rust

Now, that was request. For responses it's basically the same, in a way. Your handled has to return something that can be converted into a response. It’s literally impl IntoResponse.

Axum provides implementations for a few things like tuples of relevant pieces of information. You can return a bag or response pieces and it will glue them together into a proper thing.

It can be a redirect, or a status code and HTML string (HTML(“my html”), or a JSON string (JSON(serialized_json)). You can add headers in there. It can be a Result so you can propagate errors around.

It's very neat when it clicks but it's very hard to find out what actual types have that trait implemented for. So you have to gerp examples and dive deep in to the framework code to see if the trait is implemented for what you want to return or you have to come up with your own type and implement the trait for it.

It's not a good learning experience.

#Axum #Ruby #RubyOnRails #Rust

Since we're talking about responses let’s address the elephant in the room: HTML.

In Ruby pretty much every framework support some form of templating. Even barebones Sinatra gives you ERB out of the box and pretty much any other template engine is only one line in the gemfile away.

Axum doesn't provide any templating. You’ll have to bring your own.

I picked #Askama as it was the first templating engine on Are We Web Yet.

It's a dialect of Jinja. But maybe not 100% compatible. No matter.

One thing you learn fast is that in Rust everything is a type. So your template has to have an actual template and a struct that provides data for the template. You can't just put random code in your templates. On one hand it’s great as you can't by accident have n+1 queries in your template. On the other, to change something on the page you might to edit three files: the actual template, the struct for the template, and the handler to populate the struct.

There's no support for layouts. Well, there's kinda. Through template inheritance. It’s where you extend your layout by overriding some blocks in it. It's much more awkward than in Ruby. In Ruby layouts are conceptually separate. In Askama your template struct has to provide all the data for the template and all its parent templates. I don't even know if it's possible to render the same template in different layouts. The thing that is extremely easy in Ruby.

There's also no support for partials. At best you can insert renders of other templates. But that's awkward for many of the same reasons (mostly around providing data for the templates).

One major downside is that Askama uses proc macro to compile the templates to Rust. It makes it extremely fast but debugging templates is a nightmare. It gives you an error that something is missing or has a wrong type but doesn't tell you what it is or where in the template it is. Even with enabled proc macro backtraces it gives you a snippet of generated Rust code and not the template. Even the barebones ERB will give you the exact position in the template in case of an error.

So yeah, lots of frustration.

#Axum #Ruby #RubyOnRails #Rust

BTW, there's no form builder in Axum.

I’m not very fond of Rails form builder. It's bloated. It hides a lot of HTML features. But if you need a basic form it's just magical.

Axum also can only parse flat forms. By flat I mean, that only one level of struct nesting. Nothing like nested attributes in Axum. You have to bring your own crate for that.

#Axum #Ruby #RubyOnRails #Rust

Axum also doesn't provide any DB layer. You’ll have to bring your own. (Notice the theme?)

I’ve chosen Diesel.

At the surface it's kinda similar to ActiveRecord. You see familiar terms like migrations and associations. But in reality it's very far from AR.

Again, I'm not exactly a fan of ActiveRecord but in comparison experience working with it is all sparkly unicorns and rainbows.

Let's take migrations, for example. Migrations in Diesel are raw SQL. Which is fine is you're forking on an app that will only use a single db (like your typical startup or something will settle on, say, Postgres for the main db). It’s much more awkward when you're working on an app that people might want to deploy in different environments. Like a self-hosted app that can have 1-2 users might want to use SQLite for simplicity, and same app can be used for a company of 300 and might be better served by pg (for reliability, concurrency, backups, etc.). With Diesel you’d have to write migrations for every supported db backend. In Rails you use a simple API that abstracts that away for the most part. You still can use very specific db-dependent feature, in the same migration.

Diesel provides an option to generate migrations from a schema definition. I think it's neat. You write the schema you need and Diesel would figure out what needs to be changed and puts that in a migration. The issue is that the DSL is extremely limited. You can't even define indexes with it.

Relations in Diesel are very limited as well. It's very basic “get associated records”. Transitive associations are very awkward compared to AR.

Anther snag is in Rust everything is a type. On one hand Diesel ensures that db is in the right state to work with the types in your code. So you can be sure there won't be some weird miscommunication between the app and the db that will lose your data. On the other, you can't be loose with your requests. In Rails you can requests partial rows and you still get the same models. So you can optimise your queries much easier (but you have to be careful). In Diesel every shape of the returned row has to be a specific type. You can't randomly add or remove columns from the query.

So what’s conceptually a single model in AR can be a whole bunch of structs in Diesel. And you have to pay attention where you use which.

#Axum #Ruby #RubyOnRails #Rust

@pointlessone First of all I would like to highlight that this feedback is very valuable. It would be great if you can fill feature requests for maybe the two most relevant items at the diesel github repository, so that we could discuss how to resolve them in details.
@pointlessone
That written I also would like to highlight that you are comparing a well established and rather large project (active record) with a number of comparable "younger" projects that are essentially run by single persons. Sure, you as a user expect that everything is there, but the hard truth often is: It needs someone that cares for a certain feature and that is willing to implement and maintain it or at least fund that work.

@pointlessone Finally I would like to comment on specific points as well:

SQL migrations: At the time of writing Diesel prefers those as they are always a bit more powerful to whatever DSL you could build. That doesn't mean that it needs to stay that way. You can extend the whole migration framework outside of diesel and you are also welcome to contribute certain specific improvements (like indices) back to diesel itself.

@pointlessone

Everything is a type: That's possibly the only thing that's by design. Rust prefers to being explicit about types "everywhere". Now that doesn't necessarily mean that it is as restrictive as you describe it in your posts. Diesel offers several derives (Selectable, HasQuery, Insertable, AsChangeset) that adjust the query automatically if you add or remove fields from the struct.

@weiznich I saw the derives in examples. I even read docs for some of them. I'm still mostly clueless.

My main point of confusion why they need to be derived individually? I mean, in Ruby you have everything all the time. These derives being optional implies that there are cases when not having them is useful. That wasn't mentioned anywhere in the docs (or have I missed it?).

Diesel leans heavily on code generation but it's hard to understand how different parts interact. But I guess, it's just a skill issue for me.

@pointlessone macros, like these derives are important in rust as they are essential the only way to have some sort of code generation. I‘m not that familiar with ruby, but as far as I know it’s an interpreted language. I assume that there is some sort of introspection API, which lets libraries query information about types at runtime. Rust as language has no such capability. There are now experimental libraries which do something like that.

@pointlessone Now for diesel in particular these derives are not required for several reasons:

* Adding a derive you don’t need generates code that you don’t need which has an impact on compile times. So some people want to avoid them in some situations
* Technically speaking the derives generate a more or less fixed translation. By making them not-required you always can provide your own custom implementation if required

@pointlessone As for when to use which derive: Diesel has guides for most of them now. Reading them all might help, even if it’s a lot of stuff.
Additionally there are these (https://blog.weiznich.de/rustweek_2025.html#/title-slide) workshop slides from last year containing a lot more context. Finally the API documentation contain at least a list of support options and often examples for all of the derives.
Finally: Yes the documentation could still be better, but again that’s a question of dev-capacity
Diesel Workshop

@weiznich Ooh, nice! Thank you for the link.
@pointlessone It’s also linked from the official web page. Do you have suggestions to make it more visible?
@weiznich Not at the moment. As I said, I'm trying to learn a whole bunch of things at once. I’m probably just overwhelmed.
@pointlessone From my point of view you are in a really good situation to give such suggestions. New users tend to struggle a lot and leave nearly no feedback. So any feedback and any idea/suggestion you get from such people is very valuable as you can be certain that a lot others are also struggling with the same problem. Users that are already familiar with the library/.. tend to not notice these kind of things as there are „obviously that way“