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

BTW, remember forms? Those have to be separate structs, too.

Where in Rails you’d have Post.find(id).update(params) you'd have three different structs in Rust: one for the form, one for the partial update, and one for the returned post.

So in addition to those multiple “query” structs, you also may get multiple input structs (for each different form, for each schema in the API, etc.). And you have to com up with conversions between those for everything to work.

#Axum #Ruby #RubyOnRails #Rust

At this point I’m nearly at my wits end trying to build a form for a record that has multiple associated records.

I haven't yet touched assets. I've seen solutions to embed assets into the compiled binary, which is neat. I like that the whole app can be deployed in a single file. I suspect the build for those assets has to be external.

I also haven't touched background jobs. As far as I can tell there's nothing like Sidekiq for Rust. Though maybe there's something decent. I haven’t looked yet

#Axum #Ruby #RubyOnRails #Rust

Oh, right, almost forgot. Axum provides router but doesn't provide anything to generate URLs. And as far as I can tell, it's on purpose.

In rails you get named routes and helpers to generate URLs for them. It a little thing but it helps a lot when you change the URL but keep the name. You don't have to go through the whole app and change it everywhere but you still get the new URLs.

In Axum it’s all just strings. You have to make sure you remember every place you have that URL and you’ll have to fix it manually.

TBH, I'm baffled by this. Everywhere else everything has to be its own type and types have to be coherent. But here it's completely detached and suddenly stringly typed.

#Axum #Ruby #RubyOnRails #Rust

OK, that's a wrap for now.

My current impression is that Rust is decades behind Ruby in terms of developer experience. There are some neat ideas, some features that are only possible because of Rust's type system, performance is definitely incomparable. But the things that are called a framework in Rust would never be called that in Ruby. Rust can not be compared to Ruby in terms of development speed.

I can not stress that enough. What's done in Rails with a scaffold and a few lines of code took me like a solid week and I'm still nowhere near the end of it. Yes, I have to learn a lot. But I have to learn a lot precisely because basics are not covered in the docs and I can't copy-paste pieces to have the same (or analogous) thing as in Rails.

It's rather the basics are different. In Rails basics are CRUD. In Rust basics are how to run a router on top of a socket. A form to update a record in the db is an advanced topic in Rust web development. Unfortunately.

#Axum #Ruby #RubyOnRails #Rust

A bonus entry: error handling.

This is more of a Rust thing, not specific to Axum. So Rust has no exceptions. Instead rust has a Result type that can be either a "good" return value or an "error”.

My fellow Rubyists probably know of this concept from other languages or maybe you’ve encountered Railway-Oriented Programming pattern (it resurfaces once in a while, here's a recent one: https://www.alchemists.io/articles/railway_pattern ).

Anyway, one quirk is that since it's just a normal value, it doesn't have a backtrace attached to it. It can bubble up all the way to the main function and you wouldn't be any wiser where it came from.

Another “inconvenience” is that Result<T, E> is generic but it also means that every concrete variant of both parameter types yields a completely separate type. There's no inheritance, you have to specifically address every instance of the Result type. Where in Ruby you can handle all exceptions by catching StandardError, you can't do that in Rust.

The idiomatic solution is to have your own error type that wraps other error types and implement conversion from those error types to your own error type.

There are a few crates (packages, like gems) that try to address various aspects of this. I settled on rootcause which sorts out the backtrace deficiency. It allows for even more reach contexts attached to the errors. This is even better than in Ruby. It's always obvious in Rust where you can potentially get an error so it’s easy to provide relevant context.

However it only partially addresses the multitude of errors issue. I still had to implement my own error type to wrap around rootcause's Report because Rust doesn't allow to implement external traits (like Axum's IntoResponse) for external types (like, rootcause's Report). So in order for my handlers to be able to return proper Results that Axum could turn into responses I have to have this intermediate glue type.

But it let me have error pages with backtraces like in Rails, which is neat. But again, it's not a built-in feature, it's something I had to build myself.

I suspect there's nothing like errbit/sentry/rollbar. What do people use to catch failures in production? Anything for monitoring, metrics?

#Axum #Ruby #RubyOnRails #Rust

Railway Pattern | Alchemists

A collective devoted to the craft of software engineering where expertise is transmuted into joy.

@pointlessone Yes, proper mature frameworks for web backend don't exist in Rust. rocket is the only somewhat well-known one that even tries, and that's not seeing all that much development AFAIK.

I was actually going to comment that it's wrong to think of axum as a framework, went to our docs just to make sure we're not using that term and... it's right there in the first paragraph. Certainly going to change that.

@pointlessone Thanks again for your feedback :)
Posted a PR now to remove the 'framework' wording. Feedback welcome!
https://github.com/tokio-rs/axum/pull/3616
@jplatte TBH, didn't expect that. In the age of overhyping everything this is impressive.

@pointlessone "My current impression is that Rust is decades behind Ruby in terms of developer experience."

SAY IT AGAIN! 📣📣📣

That's why I'm working on ratatui_ruby, so folks can make TUIs in a better language than Rust or Go.

"What's done in Rails with a scaffold and a few lines of code took me like a solid week and I'm still nowhere near the end of it."

Yuuuup. Ratatui has so few batteries included that ratatui_ruby is going to have to turn into an ecosystem of libraries to get productive.

@pointlessone Hey, axum maintainer here. If you're interested in more type system enforcement around routes, you can try the typed-routing feature in axum-extra. axum-extra is a little "sidekick" crate to axum that is maintained by the same people, but contains less "stable" APIs (stuff that we may want to make breaking changes to more often - though in the typed-routing case not much has happened since its introduction IIRC).
@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.

@pointlessone Also all of these structs are designed to be shared with other derives like the axum Form or Query derive. So the point about yet another separate struct layer raised in your next post should be addressable by such a design.

Finally I'm also want to state that I'm more than happy to give more specific pointers for specific problems, so feel free to just ask questions.

@weiznich OK. Since you volunteered… 😉

AR has this cool feature that you can manipulate a record and its relations and then save the whole thing at once. This is especially useful when you do some non-trivial changes to the relations (even transitive ones).

For example, let's say I have some Inventories. Inventory has some attributes of their own (say, a name, for simplicity). Inventory also has a bunch of Items. Each Item has some attributes as well. In AR I can change Inventory name, change some Item attributes, delete some of the Items, add new Items. Then save the Inventory and AR would figure out what SQL to execute. It’s going to be an update to the inventories table, a delete to items table to remove items, an update to items to edit the changed ones, and an insert to items to save the new items. All in a transaction.

Is this something Diesel can do? Is there an example I could refer to?

@pointlessone That’s not something that is currently implemented in diesel. Due to the difference in how the languages work (missing introspection/reflection) it’s much harder to implement that in ruby. Nevertheless I think it might be possible to have something in diesel at some point.
Finally there are people in the community experimenting with extension which might help here. For example https://github.com/LucaCappelletti94/diesel-builders might help here.

@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“

@weiznich OK, it took me a bit to get through but this is great! It answered a few questions I had before. In no particular order:

  • It emphasises that it's a query builder foremost. The site says it's an ORM (first) and a query builder (second). In conjunction with other ORM terms it set my expectations closer to AR than is useful.
  • It explains that mapping is done based on column order, not name. This explains the function of Selectable.
  • Clarifies migration-schema relationship. It wasn't clear which one is primary. Turns out there are two distinct workflows that makes either migrations or the schema primary. From my previous exposure to docs it wasn't clear that I have to make that choice.
  • This solidified the idea that structs are query types and not models. In AR (and ROM to a degree but less so) model is the central concept. Model is also a singular representation of a table. E.g. User is a representation of a record in users table. It doesn't matter if it's partially loaded, if it's persisted, or not, or even deleted. It's always the same object. Whatever query you might want to execute you always word with only one model (99% of the time, you can have multiple models for a single table but it's a very specific and rare optimisation). A domain model is never thought of as a set of all possible fields combinations and permutations. Query types, on the other hand, are very specific to concrete queries and there can be as many of them as different shapes of the data in the queries.
  • You mention that some dbs don't allow schema changes in a transaction but SQLite is fine with that. Which is true but there's one thing SQLite doesn't do in a transaction: set pragmas. This is important for db tuning as defaults are not the best, at least for web apps with multiple connections.

You asked how you can make the links more prominent. So it took me a hot minute to find them. I guess it's technically true that they're third-party resources but since you're a Diesel maintainer you might as well donate them to the project. Maybe?

TBH, I think this should be in the guides. Like, take the slides and notes, put them in full sentences of flowing text and you're good to go. The first section should be Getting Started. Well, OK, it's a bit too long for that so maybe keep the current as is but make this the second guide. Testing section would be a good guide too. Extending is sort of there already so maybe add the stuff from the workshop to that guide. It's a bit early for me to comment on that section but the intro is fantastic!

@pointlessone Thanks for this feedback. That's really helpful.

For the slides: Yes the plan is to have all of this in the guides at some point. It's "just" that this takes time and effort. I did not have the capacity for that yet. Any help there would be welcome.

@weiznich I understand the sentiment, but—please don't take it personally—it doesn't hold.

Sure, AR is much older and much more effort went into it. But let's take a look at ROM. It started in 2012, 3 years before Diesel. But it wasn't being worked on since around 2020 so even less time went into it. It too was developed by mostly 2 people. But it seems to be much more feature rich. It's got much fancier relations. It's even got cross-db relations. It can use plain files (YAML, JSON, CSV, etc.), relational DBs, and NoSQL DBs all together, transparently. It's much rougher than AR, sure. Especially in the ecosystem department as AR has a whole bunch of third-party extensions. But it's still feels much more powerful than Diesel.

I'm not trying to bash on you or Diesel. Just pointing out that the effort argument doesn't hold that well.

It's true that I had expectation going in. It's also true that they most likely were uncalibrated.

@pointlessone The point I wanted to make still stands as far as I see. The example you mad are two people working full time on a framework. For diesel it’s more like one person working part time (like a day per week) on the project. That’s quite an important distinction in my opinion.

Also I would like to point out that people work on what they personally need: For me that’s less convenient top level methods but often low level methods and performance optimisation.

@weiznich It is an important distinction but it’s not the case with ROM. It was and still is your typical evenings and weekends open source. Anyway.
@weiznich I fully admit that I don’t have a good mental model for Diesel yet. And I understand that frustration comes from mismatched expectations and that is my doing. I try different models and see which fits or if I need a new one altogether. However, frustration is still there. Even if it’s mostly not Diesel’s fault.
@pointlessone There's a comprehensive list of implementations of IntoResponse here, generated automatically by rustdoc: https://docs.rs/axum/latest/axum/response/trait.IntoResponse.html. It's divided into implementations on types defined outside the crate ("foreign" types) and those defined inside.
IntoResponse in axum::response - Rust

Trait for generating responses.

@blp it’s technically there but how useful is it? An impl for a tupple of 16 generic types with no doc string is something to get used to. Even source is rarely helpful as it’s implemented in terms of conversion to other types.

Reading these docs is a whole skill I’m still developing.

@pointlessone It is a skill!

When I look at the list of impls there, what I see is that some are individually relevant and then a lot more are there for convenience working with tuples of various lengths. The constraints on the generic parameters to the impls are informative (for example, seeing IntoResponseParts on tuple members immediately helps me to understand), and the source links are available too.

Trait impls rarely have their own documentation comments. Perhaps they should more often.