Farewell, Rust for web
yieldcode.blogI find the dependency creep for both rust and node unfortunate. Almost anything I add explodes the deps and makes me sweat for maintenance, vulnerabilities, etc. I also feel perpetually behind, which I think is basically frontend default mode. Go does the one thing I wish Rust had more of which is a pretty darn great standard library with total backwards compatibility promises. There are awkward things with Go, but man, not needing to feel paranoid and how much can be built with so little feels good. But I totally understand just getting crap done and taking off the tin foil. Depends on what you prioritize. Solo devs don't have the luxury.
Those deps have to come from somewhere, right? Unless you're actually rolling your own everything, and with languages that don't have package managers what you end up doing is just adding submodules of various libraries and running their cmake configs, which is at least as insecure as NPM or Crates.io.
Go is a bit unique a it has a really substantial stdlib, so you eliminate some of the necessary deps, but it's also trivial to rely on established packages like Tokio etc, vendor them into your codebase, and not have to worry about it in the future.
The tradeoff Go made is that certain code just cannot be written in it.
Its STD exists because Go is a language built around a "good enough" philosophy, and it gets painful once you leave that path.
> The tradeoff Go made is that certain code just cannot be written in it.
Uh... yeah? That's true of basically all platforms, and anyone who says otherwise is selling something.
> it gets painful once you leave that path
Still less painful than being zero-day'd by a supply chain attack.
> > The tradeoff Go made is that certain code just cannot be written in it.
> Uh... yeah? That's true of basically all platforms, and anyone who says otherwise is selling something.
What code can you not write in C?
Might be painful for some(many) cases, but there is nothing you can't write in C.
SIMD code.
And if you are going to point out compiler extensions, they are extensions exactly because ISO C cannot do it.
not really in-topic but constant-time crypto primitives are considered hard for any compiled language with a lot of optimizations
It is more of a cultural thing. Package managers encourage lots of dependencies while programmers using language with no package managers will often pride themselves in having as few dependencies as possible. when you consider the complete graph, it has an exponential effect.
It is also common in languages without package managers to rely on the distro to provide the package, which adds a level of scrutiny.
Technically it's the same. But behaviorally it's not. When pulling in more dependencies is so easy, it's very hard to slow down and ask the question do we need all of this?
Mucking around with cmake adds enough friction that everyone can take a beat for thoughtful decision-making.
> Those deps have to come from somewhere, right? Unless you're actually rolling your own everything
The point is someone needs to curate those "deps". It's not about rolling your own, it's about pulling standard stuff from standard places where you have some hope that smart people have given thought to how to audit, test, package, integrate and maintain the "deps".
NPM and Cargo and PyPI all have this disease (to be fair NPM has it much worse) where it's expected that this is all just the job of some magical Original Author and it's not anyone's business to try to decide for middleware what they want to rely on. And that way lies surprising bugs, version hell, and eventually supply chain attacks.
The curation step is a critical piece of infrastructure: thing things like the Linux maintainer hierarchy, C++ Boost, Linux distro package systems, or in its original conception the Apache Foundation (though they've sort of lost the plot in recent years). You can pull from those sources, get lots of great software with attested (!) authorship, and be really quite certain (not 100%, but close) that something in the middle hasn't been sold to Chinese Intelligence.
But the Darwinian soup of Dueling Language Platforms all think they can short circuit that process (because they're in a mad evangelical rush to get more users) and still ship good stuff. They can't.
I mean somebody could make a singular rust dependency that re-packages all of the language team's packages.
But what's the threat model here. Does it matter that the Rust STD library doesn't expose say "Regex" functionality forcing you to depend on Regex [1] which is also written by the same people who write the STD library [2]? Like if they wanted to add a back-door in to Regex they could add a backdoor into Vec. Personally I like the idea of having a very small STD library so that it's focused (as well as if they need to do something then it has to be allowed by the language unlike say Go Generics or ELM).
Personally I think there's just some willful blindness going on here. You should never have been blindly trusting a giant binary blob from the std library. Instead you should have been vendoring your dependencies and at that point it doesn't matter if its 100 crates totaling 100k LOC or a singular STD library totaling 100k LOC; its the same amount to review (if not less because the crates can only interact along `pub` boundaries). [1]: https://docs.rs/regex/latest/regex/
> I mean somebody could make a singular rust dependency that re-packages all of the language team's packages.
That's not the requirement though! Curation isn't about packaging, it's about independent (!) audit/test/integration/validation paths that provide a backstop to the upstream maintainers going bonkers.
> But what's the threat model here.
A repeat of the xz-utils fiasco, more or less precisely. This was a successful supply chain attack that was stopped because the downstream Debian folks noticed some odd performance numbers and started digging.
There's no Debian equivalent in the soup of Cargo dependencies. That mistake has bitten NPM repeatedly already, and the reckoning is coming for Rust too.
Isn't xz-utils exactly why you would want a lot of dependencies over a singular one?
If say Serde gets compromised then only the projects depending on that version of Serde are as opposed to if Serde was part of the std library then every rust program is compromised.
> That mistake has bitten NPM repeatedly already, and the reckoning is coming for Rust too.
Eh, the only things that coming is using software expressly without a warranty (expectantly) will mean that software will cause you problems at an unknown time.
> A repeat of the xz-utils fiasco
Wasn't that a suspected state actor? Against that threat model your best course of action is a prayer and some incense.
Notably, xz utils didn't use any package manager ala NPM and it relied on package management by hand.
> because the downstream Debian folks
Not sure what you mean by this, but this was discovered by a Postgres dev running bleeding edge Debian. No Debian package maintainer noticed this.
> There's no Debian equivalent
How would Debian approach help? Not even their maintainers could sniff this one.
There exists a sort of extended std library of Rust dep. But no one is using it.
> Go is a bit unique a it has a really substantial stdlib
It’s not that unique though. I can say that Python and hell, even PHP have pretty complete but also well documented stdlib.
Java is meh tier but C# is also pretty good in this aspect.
It’s totally a choice for Rust not to have a real stdlib and actually I feel like that would maybe make Rust maybe the best language overall.
java didn't have an http client (I guess it had a url 'stream') for the longest time and STILL doesn't have an http server.
It has one - it’s been a part of the JDK for a while https://docs.oracle.com/en/java/javase/11/docs/api/jdk.https...
sun is typically not available anymore / deprecated even and not available in the JRE or it's early access, but fair point.
java didn't have an http client [...] and STILL doesn't have an http server.
Wow.
How long has it been since you guys have used Java?
Serious question?
I tried to implement a minimal server just to realize that there is still no way to do so in java 21... I stand corrected I guess it was recently added: https://docs.oracle.com/en/java/javase/25/docs/api/jdk.https..., but it's a sun package instead of standard RT - but probably because it is still early.
When I run into things like this that I know are wrong, I try to remember when reading things I don't know about...
Not really? Iirc `HttpUrlConnection` has been around since the 90s?
I did mention that, but for a lot of things it is not enough compared to a full http client most stdlib's have. HttpClient was introducted for a reason.
Python used to have a great standard library, too. But now it's stuck with a bunch of obsolete packages and the packaging story for Python is awful.
In a decade or so Go the awkward things about Go will have multiplied significantly and it'll have many of the same problems Python currently has.
> the packaging story for Python is awful.
Big caveat that this is just for me personally, but uv has fixed this for me personally. Game changing improvement for Python. Appropriately, uv is written in rust.
The fact that you have to know to use uv rather than any of the other package managers is kind of the point.
Lots of removals have already happened and uv took over packaging in Python-land.
Which, ironically, is written in rust
Well, Python is largely written in C, so there's that.
I just ported (this week) a 20-year-old Python app to uv/polars. (With AI it took two days). App is now 20x faster.
that's polars for ya
uv should not impact runtime performance at all
Both uv and polars are technically Rust, too.
These are two sides of the same coin. Go has its quirks because they put things in the standard library so they can't iterate (in breaking manners), while Rust can iterate and improve ideas much faster as it's driven by the ecosystem.
Edit: changed "perfect" to "improve", as I meant "perfect" as "betterment" not in terms of absolute perfection.
The golang.org/x/ namespace is the other half of the standard library in all but name. That gets iterated often.
For stuff in the standard library proper, the versioning system is working well for it. For example, the json library is now at v2. Code relying on the original json API can still be compiled.
There is a moral hazard here. By accepting that APIs are forever, you tend to be more cautious and move toward getting it right the first time. Slower is better... And also faster in the long run, as things compose. Personally, I do believe that there is one best way to do things quite often, but time constraints make people settle.
At least it is my experience building some systems.
Not sure it is always a good calculus to defer the hard thinking to later.
Iterating often is not helpful for stable systems over time.
I like go's library it's got pretty much everything needed out of the box for web server development. Backwards compatibility is important too.
The cost of "perfecting" an idea here is ruining the broader ecosystem. It is much much better for an API to be kinda crappy (but stable) for historical reasons than dealing with the constant churn and fragmentation caused by, for example, the fifth revision of that URL routing library that everyone uses because everyone uses it. It only gets worse by the orthogonal but comorbid attitude of radically minimizing the scope of dependencies.
Which has been working great for go, right. They shipped "log" and "flag" stdlib packages, so everyone uses... well, not those. I think "logrus" and "zap" are probably the most popular, but there's a ton of fragmentation in Go because of the crappy log package, including Go itself now shipping two logging packages in the stdlib ('log/slog').
Rust on the other hand has "log" as a clear winner, and significantly less overall fragmentation there.
I think you underestimate how many programs use log and flag, if you just focus on the few (bloated) popular projects.
> It is much much better for an API to be kinda crappy (but stable) for historical reasons
But this does more than just add a maintenance burden. If the API can't be removed, architectural constraints it imposes also can't be removed.
e.g. A hypothetical API that guarantees a callback during a specific phase of an operation means that you couldn't change to a new or better algorithm that doesn't have that phase.
Yes you can, and Go has done exactly that.
Realize the "log" api is bad? Make "log/slog". Realize the "rand" api is bad? Make "rand/v2". Realize the "image/draw" api is bad? Make "golang.org/x/image/draw". Realize the "ioutil" package is bad? Move all the functions into "io".
Te stdlib already has at least 3 different patterns for duplicating API functionality with minor backwards-incompatible changes, and you can just do that and mark the old things as deprecated, but support it forever. Easy enough.
> mark the old things as deprecated, but support it forever
Is that 'supported'? A library that uses a callback that exists in 'log' but not in 'slog'; it'll compile forever, but it'll never work.
'Compiles but doesn't work' does not count as stable in my book. It's honestly worse than removing the API: both break, but one of them is noticed when the break happens.
I think “the fifth revision of that URL routing library that everyone uses” is a much less common case than “crate tried to explore a problem space, five years later a new crate thinks it can improve upon the solution”, which is what Rust’s conservatism really helps prevent. When you bake a particular crate into std, competitor crates now have a lot of inertia to overcome; when they're all third-party, the decision is not “add a crate?” but “replace a crate?” which is more palatable.
Letting an API evolve in a third-party crate also provides more accurate data on its utility; you get a lot of eyes on the problem space and can try different (potentially breaking) solutions before landing on consensus. Feedback during a Rust RFC is solicited from a much smaller group of people with less real-world usage.
Is there that much to explore in a given problem space. I believe a lot of people will take the good enough, but stable API over the unstable one that is striving for an unknown state of perfection. The customer of a library are programmers, they can patch over stuff for their own use case. A v2 can be released once enough pain points have been identified, but there should be a commitment to support v1 for a while.
The dependency creep keeps on happening in web frameworks where ever you look.
I was thinking of this quote from the article:
> Take it or leave it, but the web is dynamic by nature. Most of the work is serializing and deserializing data between different systems, be it a database, Redis, external APIs, or template engines. Rust has one of the best (de)serialization libraries in my opinion: serde. And yet, due to the nature of safety in Rust, I’d find myself writing boilerplate code just to avoid calling .unwrap(). I’d get long chain calls of .ok_or followed by .map_err. I defined a dozen of custom error enums, some taking other enums, because you want to be able to handle errors properly, and your functions can’t just return any error.
I was thinking: This is so much easier in Haskell.
Rather than chains of `ok_or()` and `map_err()` you use the functor interface
Rust:
``` call_api("get_people").map_or("John Doe", |v| get_first_name(v)).map_or(0, |v| get_name_frequency(v)) ```
Haskell:
``` get_first_name . get_name_frequency <$> callApi "get_people" ```
It's just infinitely more readable and using the single `<$>` operator spares you an infinite number of `map_or` and `ok_or` and other error handling.
However, having experience in large commercial Haskell projects, I can tell you the web apps also suffer from the dreaded dependency explosion. I know of one person who got fired from a project due to no small fact that building the system he was presented with took > 24 hours when a full build was triggered, and this happened every week. He was on an older system, and the company failed to provide him with something newer, but ultimately it is a failing of the "everything and the kitchen sink" philosophy at play in dependency usage.
I don't have a good answer for this. I think aggressive dependency reduction and tracking transitive dependency lists is one step forward, but it's only a philosophy rather than a system.
Maybe the ridiculous answer is to go back to php.
> I know of one person who got fired from a project due to no small fact that building the system he was presented with took > 24 hours when a full build was triggered, and this happened every week.
Incremental Nix builds can take less than 1 munute to build everything, including the final deployable docker image with a single binary on very large Haskell codebases. That fact the the person was fired for everybody around him systematically failing to admit and resolve a missing piece of supportive infrastructure for the engineering effort of one person tells a lot about the overall level of competence in that team.
> but ultimately it is a failing of the "everything and the kitchen sink" philosophy at play in dependency usage.
Not really, as the kitchen sink only has to build once per its version change, for all future linkage with your software for the entire engineering team doing the builds in parallel.
Rust has trouble supporting higher-kinded types like Functor (even though an equivalent feature is available, namely Generic Associated Types) due to the distinctions it makes between owned and referenced data that have no equivalent in Haskell. Whether these higher abstractions can still be used elegantly despite that complexity is something that should be explored via research, this whole area is not ready for feature development.
24 hours? is the haskel compiler written in javascript running in a python js-interpreter written in bash?
php is the only popular language that regularly removes insane legacy cruft (to be fair, they have more insane cruft than almost any other language to begin with).
I've found Go's standard library to be really unfortunate compared to rust.
When I update the rust compiler, I do so with very little fear. My code will still work. The rust stdlib backwards compatible story has been very solid.
Updating the Go compiler, I also get a new stdlib, and suddenly I get a bunch of TLS version deprecation, implicit http2 upgrades, and all sorts of new runtime errors which break my application (and always at runtime, not compiletime). Bundling a large standard library with the compiler means I can't just update the tls package or just update the image package, I have to take it or leave it with the whole thing. It's annoying.
They've decided the go1 promise means "your code will still compile, but it will silently behave differently, like suddenly 'time1 == time2' will return a different result, or 'http.Server' will use a different protocol", and that's somehow backwards compatible.
I also find the go stdlib to have so many warts now that it's just painful. Don't use "log", use "log/slog", except the rest of the stdlib that takes a logger uses "log.Logger" because it predates "slog", so you have to use it. Don't use the non-context methods (like 'NewRequest' is wrong, use 'NewRequestWithContext', don't use net.Dial, etc), except for all the places context couldn't be bolted on.
Don't use 'image/draw', use 'golang.org/x/image/draw' because they couldn't fix some part of it in a backwards compatible way, so you should use the 'x/' package. Same for syscall vs x/unix. But also, don't use 'golang.org/x/net/http2' because that was folded into 'net/http', so there's not even a general rule of "use the x package if it's there", it's actually "keep up with the status of all the x packages and sometimes use them instead of the stdlib, sometimes use the stdlib instead of them".
Go's stdlib is a way more confusing mess than rust. In rust, the ecosystem has settled on one logging library interface, not like 4 (log, slog, zap, logrus). In rust, updates to the stdlib are actually backwards compatible, not "oh, yeah, sha1 certs are rejected now if you update the compiler for better compile speeds, hope you read the release notes".
Man, I've been using Go as my daily driver since 2012 and I think I can count the number of breaking changes I've run into on one finger, and that was a critical security vulnerability. I have no doubt there have been others, but I've not had the misfortune of running into them.
> Don't use "log", use "log/slog", except the rest of the stdlib that takes a logger uses "log.Logger" because it predates "slog", so you have to use it.
What in the standard library takes a logger at all? I don't think I've ever passed a logger into the standard library.
> the ecosystem has settled on one logging library interface, not like 4 (log, slog, zap, logrus)
I've only seen slog since slog was added to the standard library. Pretty sure I've seen logrus or similar in the Kubernetes code, but that predated slog by a wide margin and anyway I don't recall seeing _any_ loggers in library code.
> In rust, the ecosystem has settled on one logging library interface
I mean, in Rust everyone has different advice on which crates to use for error handling and when to use each of them. You definitely don't have _more standards_ in the Rust ecosystem.
> I don't think I've ever passed a logger into the standard library.
`net/http.Server.ErrorLog` is the main (only?) one, though there's a lot of third-party libraries that take one.
> I've only seen slog since slog was added to the standard library
Most go libraries aren't updated yet, in fact I can't say I've seen any library using slog yet. We're clearly interfacing with different slices of the go ecosystem.
> in Rust everyone has different advice on which crates to use for error handling and when to use each of them. You definitely don't have _more standards_ in the Rust ecosystem.
They all are still using the same error type, so it interoperates fine. That's like saying "In go, every library has its own 'type MyError struct { .. }' that implements error, so go has more standards because each package has its own concrete error types", which yeah, that's common... The rust libraries like 'thiserror' and such are just tooling to do that more ergonomically than typing out a bunch of structs by hand.
Even if one dependency in rust uses hand-typed error enums and another uses thiserror, you still can just 'match' on the error in your code or such.
On the other hand, in Go you end up having to carefully read through each dependency's code to figure out if you need to be using 'errors.Is' or 'errors.As', and with what types, but with no help from the type-system since all errors are idiomatically type-erased.
> When I update the rust compiler, I do so with very little fear. My code will still work. The rust stdlib backwards compatible story has been very solid.
This is not always true, as seen with rustc 1.80 and the time crate. While it only changed type inference, that still caused some projects like Nix a lot of trouble.
That caused compilation errors though, which are alright in my book, and don't increase my fear to update.
Silent runtime changes are what spook me and what I've gotten more often with Go.
FYI: Deno includes:
1. The web standard APIs themselves 2. It's own standard library inspired by Go's standard library (plus some niceties like TOML minus some things not wanted in a JS/TS standard library since they're already in the web standard APIs) 3. Node's standard library (AKA: built-in modules) to maintain backwards compatibility with node.
Bun has 1 and 3, and sort of has it's own version of 2 (haphazard, not inspired by go, and full of bun-isms which you may like but may not, but standard database drivers is nice).
A Tauri hello world app has about 500(?) deps out of the box, always makes me laugh.
I get that cross platform desktop app is a complicated beast but it gives off those creepy npm vibes.
Same. That’s why Go is such a great tool.
With Go it's good to keep in mind the Proverbs, which includes this gem:
A little copying is better than a little dependency.Good luck, if little copying is ICU based localization.
The whole point of "a little copying" is when there's self-contained code that can be copypasta'd -- for situations that are appropriate.
Honestly this is one of the biggest reasons I stick with Elixir. Between Elixir’s standard library, the BEAM/OTP, and Phoenix (with Ecto)—- I honestly have very few dependencies for web projects. I rarely, at this point, find the need to add anything to new projects except for maybe Mox (mocking library) and Faker (for generating bits of test data). And now that the Jason (JSON) library has been more or less integrated into OTP I don’t even have to pull it in. Elixir dev experience is truly unmatched (IMHO) these days.
I work on a large mixed Rust/C systems codebase — been converting it piece by piece for a couple years now. The author's frustrations are legit, but I think the real question is simpler: how expensive are your bugs?
If a bug in your system means silent data corruption that nobody notices for a week — and I've lived this — Rust is worth every second of compile time. If a bug means a 500 and you redeploy, you're paying for insurance you don't need. Different worlds, different tools.
The thing I actually love about Rust — and this sounds weird — is how it handles failure. In C, every function call is an implicit "and also maybe something went horribly wrong, but let's just hope it didn't." You get used to it. You stop seeing it. Then one day you're staring at a corruption bug and you trace it back to an error return that got silently swallowed six call sites ago, and you feel physically ill. Result types are annoying when you're validating form input. They're a gift from god when you're the one who has to explain why someone's data is gone.
But yeah, for web stuff? Just use TypeScript. Life's too short to fight the borrow checker over a blog.
> But yeah, for web stuff? Just use TypeScript. Life's too short to fight the borrow checker over a blog.4
I am not sure about TypeScript. I think having static typing is just too good of an insurance against stupid bug and for your own sanity. I think for web purposes, especially with LLM around, you probably should just use Go. You don't have to like it, but there's enough training dataset for your CRUD application. So all you really need to do is to be able to read it.
> I am not sure about TypeScript. I think having static typing is just too good of an insurance against stupid bug and for your own sanity.
TypeScript has static typing though?
Typescript has static typing. It’s called typescript..
the idea of one language to rule them all is very compelling. it’s been promised a lot, and now everyone hates Java.
but the truth is that Rust is not meant for everything. UI is an abstraction layer that is very human and dynamic. and i can come and say, “well, we can hide that dynamism with clever graph composition tricks” à la Elm, React, Compose, etc, but the machinery that you have to build for even the simplest button widget in almost every Rust UI toolkit is a mess of punctuation, with things like lifetimes and weird state management systems. you end up building a runtime when what you want is just the UI. that’s what higher level languages were made for. of course data science could be done in Rust as well, but is the lifetime of the file handle you’re trying to open really what you’re worried about when doing data analysis?
i think Rust has a future in the UI/graphics engine space, but you have to be pretty stubborn to use it for your front end.
Rust is "Jack of all trades, master of some".
There are real advantages to choosing a jack of all trades language for everything; for example it makes it easier for an engineer on one part of your project to help out on a different part of your project.
But it sounds like the OP didn't get any of the benefits of "jack of all trades", nor did he choose a field where Rust is "master of some".
Lisp is the master of all. Or it would be except "Parens? Eugh! Brotha, eugh!"
Lisp lost because none the the Lisperati came down from on high and deigned to explain how to use it for tasks running on the 1980s microcomputers.
Lisp also lost because the 1980s Lisperati spent all their time explaining lists and recursion over and over instead of explaining hash tables, vectors, and iteration.
Somehow, Lisp lost out to pathetically slow BASIC interpreters and C compilers that you had to swap floppies continuously for hours. That is a stunning level of fail.
How much of that is just because Rust UI libraries are not ready, though? It's never going to become Python, but it could become decent.
Better title: "Farewell, Rust for Web"
Yes. This is one of the things that drives me nuts about a lot of titles on here: the context like “for the web” changes how it’s is interpreted a great deal. I see the same thing when I see posts about other languages and AI and such. Context matters versus making it sound like a broad, general statement. Alas, the broad, general statements likely get more engagement..
Farewell, Rust for Web was a worse title. I thought it meant Rust ended WebAssembly support.
Agreed! The context matters a lot. Rust is a great language, but using it for the web is a poor choice just like using JS outside the web is a poor choice. Programming languages all have domains where they do well or poorly, and trying to make a single language work for all cases is a fool's errand.
Ok, we'll use that above. Thanks!
Yeah Astro is a great choice for a static or mostly static website. Moving to Astro is not a slight on any other language or framework.
Aiui they are also migrating their backend api(s) from rust to node. They were already using astro with rust on the backend (after dropping ssr with tera).
I'm a heavy Rust user and fan, but I'd never pick Rust for web. There are way more mature ecosystems out there to choose from. Why would you waste "innovation tokens" in a Rust-based web application?
I enjoyed using Rust/WASM for a web application I made. Once I got the build step figured out, which took a week, the application worked like I wanted right away.
I was trying to build an HTML generator in Rust and got pretty far, but I don't think I'll ever be happy with the API unless I learn some pretty crazy macro stuff, which I don't want. For the latter project, the "innovation tokens" really rings true for me, I spent months on the HTML gen for not much benefit.
For a web backend? Rust is pretty mature there, it doesn't even feel like an innovation token - it's by my favorite thing to use Rust for.
You have very mature webservers, asyncio, ORMs, auth, etc., it's very easy to write, and the type safety helps a ton.
In 2020 it might have taken some innovation tokens, but the only things that require a ton less (for web backend) are probably Java, python, and node.js, and they all have their unique pain points that it doesn't seem at all crazy?
It's been a while since I last had a detailed look at web applications in Rust (i.e., stuff with databases, auth, etc). You could use axum for the web server, which is very mature, but I'd say it's too low-level (IIRC you cannot even generate an OpenAPI spec of your endpoints, which IMO is table-stakes). Have you found something more batteries-included, with a similar level of maturity, and actively maintained by a community you can trust? It's a very high bar.
Your reply made me curious about ORMs, btw. Which one would you recommend? Maybe things have improved since I last checked. Last time I didn't like any of them and ended up settling on `sqlx` + hand-written SQL (the code is open source, hosted at https://github.com/rustls/rustls-bench-app/tree/main/ci-benc...).
I love Diesel for ORMs - it's very much in the same spirit as a type safe sqlalchemy, so it depends if you like that. The type safety is a great feature, it always saves me from writing incorrect queries (a huge one is nullability is represented as an option, so nullability mismatches are caught very early). It has an async version that is not as well maintained so it does lag behind, but I've never had an issue with it. To me personally, the Diesel ORM is the #1 reason to be using Rust for a web backend - it has saved me so much pain from having db/model mismatches since they're caught early.
The only other thing I've heard that is close in any language is C#/F# LINQ (I mean I'm sure there's random other projects, but haven't talked to other people about actually deploying backends with similarly type safe ORMs other than that).
There is axum OpenAPI at https://docs.rs/axum-openapi3/latest/axum_openapi3/, I haven't personally used it, I've mostly been doing GraphQL which I find works very well (including the N+1 problem etc).
And of course, I personally find cargo and the dependencies there to be roughly as ergonomic as python. Its dependency ecosystem for web isn't as deep as python or node.js, but it's pretty solid IMO. It may not have downloadable clients for a lot of pre-existing OpenAPIs etc., but that's also something Claude can port in 5 minutes.
Upon reflection, my comfort with sqlalchemy, diesel, C++ STL, and vibe coding all share one thing in common - I am pretty comfortable liking to code at a high level that is productive, while at the same time going deep to know what the abstractions produce under the covers. E.g. I at least spot check my vibe coded code, I spot check the assembly from C++ STL to ensure zero cost abstraction, I spot check the SQL from sqlalchemy or Diesel.
I am thinking a lot of people are not comfortable with this.
Personally, super high level abstractions where I do know what ultimately results is personally what I want to be productive - so this may color my love of Rust, vibe coding, and Diesel.
Thanks for taking the time! I love being able to see through abstractions too, but for web applications I'm looking for a bit more "magic" (e.g., .NET's EF Core is the gold standard for what I'd look for in an ORM).
Also curious about their opinion.
I've over the years began to interface with a lot of PHP code and there's a lot of really neat configuration stuff you can do. Ex. creating different pools for the incoming requests (so logged out users or slow pages are handled by the same pool). Like it seems to me for all of the rust web servers you have to still do a lot of stuff all on your own through code and it's not like you can create an existing Pool-ing struct.
I don't think it probably helps with a lot of the super easy stuff like creating a pool with a line of configuration - fair!
I (personally) would rather spend the fixed several hours of doing a few things like that manually, vs. pounding my head on the desk for impossible-to-find bugs.
> batteries-included
Rust tends to have more of the model of small packages that do one thing rather than monolithic frameworks.
e.g. as in the sibling comment, if you want openAPI you install axum-openapi, rather than being included in the framework.
I think there's a couple stories of people shipping some performant "webapp"-y stuff in Rust... but of course you can just compile those kinds of components and write the rest of your app in any other system
Good to know! You probably saved me a lot of pain.
I would assume today that maybe Dioxus or Leptos would be considered. Though that would be the "all in" approach on Rust front to back... it wouldn't really reduce some of the handling conditions levied in the article though.
I find C# can be a really good middle ground on the backend (not a blazor fan)... the syntax and expressiveness improves with every release. You can burrow as lot of patterns from the likes of Go as well as FP approaches. What I don't care for are excessively complex (ie: "Enterprise") environments where complexity is treated like a badge of honor instead of the burden of spaghetti that it is in practice.
due to the nature of safety in Rust, I’d find myself writing boilerplate code just to avoid calling .unwrap(). I’d get long chain calls of .ok_or followed by .map_err. I defined a dozen of custom error enums, some taking other enums, because you want to be able to handle errors properly, and your functions can’t just return any error.
This can be a double edged sword. Yes, languages like python and typescript/JavaScript will let you not catch an exception, which can be convenient. But that also often leads to unexpected errors popping up in production.
Often is not the word I'd use, from my experience.
The times something like that happened to me AND wasn't a trivial fix can be counted on half a hand. A tradeoff I'd take any day to not have to deal with rust all of the time.
Java exceptions usually have way more context to the point where I loathe every single developer who decided to catch an exception and only log the top level message.
Having a long ass chain of 10 nested exceptions might be overwhelming to a beginner, but an experienced developer knows which types of exceptions are caused by what and instinctively tunes out the irrelevant ones and goes straight to the source of the problem since the stack trace directly tells you which chain of calls caused the issue.
It's a throwaway comment in the article, but I feel it's important to push back on: HTML is very definitely a programming language, by any reasonable definition of "programming language".
Edit to add: It might not be an imperative language, but having written some HTML and asked the computer to interpret it, the computer now has a programmed capability, determined by what was written, that's repeatable and that was not available apart from the HTML given. QED.
HTML requires you to understand symbolic representations, where <> means something special. It is more verbose, but no more structurally complex, than Markdown. It does not require you to understand imperative dynamic logic. Getting the hang of symbolic representations is easy, and getting the hang of imperative programming is very hard and most people can't do it. That's why the dividing line is where it is. Making a static bulleted list isn't a 'capability' in HTML if you weren't thinking it was one in Markdown, and inventing your own precisely crafted definition with no purpose other than to include HTML then calling all others unreasonable doesn't convince anyone.
How would one do an if condition or enumerate a list in HTML alone? For that functionality you need another language to generate/manipulate the HTML.. not to mention interpreting HTML for display.
HTML is a markup language, it's even in the name... but it's not a complete programming language by any stretch.
It's not Turing-complete, and as you say, it's a markup language and it's not general purpose. But neither is a necessary component of "programming language".
Ifs and enumerations are a simpler requirement than Turing completeness. They're an even more basic version of giving the computer logic to evaluate.
Exactly... it's pretty much what I consider the minimum for a "programming language" is that you need to be able to have basic state and be able to make use of state.
For that matter, it wouldn't take much to get HTML to have those features... though the DOM, JS and even WASM do so well, we don't need it generally speaking.
Please explain how your edit doesn't apply to a .txt file
agreed, it's a hill i am very willing to die on too.
So is Markdown a programming language? Any logic for html, is therefore Markdown as well.
sure, it's a dsl for generating formatted output
If we trim markdown to just italic, bold, and underline, is it still a programming language?
What if we trim even further, to just the ASCII control codes? My newline characters make the computer perform a special action to generate formatted output. Is that programming?
if we trim a regex down to literal character matches is it still a regex?
So any text file is a programming language?
I want to address this one point:
> Similar thing can be said about writing SQL. I was really happy with using sqlx, which is a crate for compile-time checked SQL queries. By relying on macros in Rust, sqlx would execute the query against a real database instance in order to make sure that your query is valid, and the mappings are correct. However, writing dynamic queries with sqlx is a PITA, as you can’t build a dynamic string and make sure it’s checked during compilation, so you have to resort to using non-checked SQL queries. And honestly, with kysely in Node.js, I can get a similar result, without the need to have a connection to the DB, while having ergonomic query builder to build dynamic queries, without the overhead of compilation time.
I've used sqlx, and its alright, but I've found things much easier after switching to sea-orm. Sea-orm has a wonderful query builder that makes it feel like you are writing SQL. Whereas with sqlx you end up writing Rust that generates SQL strings, ie re-inventing query builders.
You also get type checking; define your table schema as a struct, and sea-orm knows what types your columns are. No active connection required. This approach lets you use Rust types for fields, eg Email from the email crate or Url from the url crate, which lets you constrain fields even further than what is easy to do at the DB layer.
ORMs tend to get a bad reputation for how some ORMs implement the active record pattern. For example, you might forget something is an active record and write something like "len(posts)" in sqlalchemy and suddenly you are counting records by pulling them from the DB in one by one. I haven't had this issue with sea-orm, because it is very clear about what is an active record and what is not, and it is very clear when you are making a request out to the DB. For me, it turns out 90% of the value of an ORM is the query builder.
You appear to have entirely confused sqlx and diesel. You don't 'feel like' you're writing SQL in sqlx, you are writing SQL in sqlx.
When queries get complicated enough, you end up writing a custom query builder to build your SQL.
Ceci n'est pas une pipe
sqlx doesn't build queries, or at least it minimally builds them. Which I think is the thing the OP is complaining about.
And, IMO, making dynamic queries harder is preferable. Dynamic queries are inherently unsafe. Sometimes necessary, however you have to start considering things like sql injection attacks with dynamic queries.
This isn't to poo poo sea-orm. I'm just saying that sqlx's design choice to make dynamic queries hard is a logical choice from a safety standpoint.
They didn't make them hard by design, I think, it's just the limitations of the current API and prioritisation. Dynamic queries are possible, just not trivial
Nope, it really was part of the design [1]
[1] https://github.com/launchbadge/sqlx/issues/333#issuecomment-...
> And, IMO, making dynamic queries harder is preferable. Dynamic queries are inherently unsafe. Sometimes necessary, however you have to start considering things like sql injection attacks with dynamic queries.
Depends on what you mean by "dynamic query". You are dealing with injection attacks as soon as you start taking user input. Most useful user facing applications take user input.
In a simple case it might be "SELECT * FROM posts WHERE title LIKE '%hello world%', where "hello world" is a user specified string. This is easy with sqlx. Where things get more difficult is if you want to optionally add filters for things like date posted, score of the post, author, etc... That makes the query dynamic in a way that can't be solved by simply including a bind.
That's where sea-orm shines over sqlx IMO. sqlx will force you to do something like
```
let mut my_query = "SELECT * FROM posts WHERE title LIKE '%' + $1 + '%'";
let mut my_binds = vec![args.keyword];
if let Some(date) = args.date {
}my_query = format("{my_query} AND date = $2"); my_binds.push(date);...
```
Your building a string and tracking binds. It gets messy. A good query builder like seaorm has lets you do something this:
```
let mut query = Posts::find().filter(Column::title::like(args.keyword));
if let Some(date) = args.date {
}query = query.filter(column::Date::eq(date));```
This pays off as your queries get more complicated. It pushes the string manipulation and bookkeeping into a library, which can be more thoroughly tested.
It also lets you pass around typed partial queries, eg in the example above query might be returned from a function, which helps you build more modular code.
I agree with what you are saying, this is exactly what I was thinking when I said it was sometimes necessary. It's just not preferable IMO.
For this specific example, the better way is something like this
But I get how this would be untenable if as the number of query param combos goes up. In that case dynamic SQL really is the only sane way to handle something like that.let result = if let Some(date) = args.date { sqlx::query("SELECT * FROM posts WHERE title LIKE '%' + $1 + '%' AND date = $2") .bind(args.keyword) .bind(date) .fetch() } else { sqlx::query("SELECT * FROM posts WHERE title LIKE '%' + $1 + '%") .bind(args.keyword) .fetch() }
The TS/React ecosystem is so mature, it's hard for Rust to compete with it. My optimal stack is currently: Rust on the backend, Typescript/React for web with OpenAPI for shared types.
React and its ecosystem is a pile of garbage perpetuated by industry inertia. UseState, useMemo, useThisAndThat where you have to guess whether that dependency will cause a re-render? Or 20 different routers, state managers, query builders? I'm not even talking about html-in-ts with `!!a && (<div>...</div>)` A stodgy, bloated, overhyped and misused monstrosity, that's what React is.
useMemo is definitely a scourge on my existence. Doesn't help that a bunch of people write articles like "don't bother with it!!" when memoisation results can cause actual real bugs when integrating with a third party lib.
Unmounting and then remounting the same component is actually a bad thing when you lose your component state in the process. And when you have enough useEffect's in your system that's exactly what happens unless you're liberally sprinkling useMemo
React is opinionated. The whole point of the library is having UI updates being driven by state mutation. When I hear complain about the hooks, I ask about what is the state, and where do mutations occur, and usually, I get blank stares in returns.
It's all about the state. `useState` is the starting point (adding new items to the state set), `useEffect` for tying the UI to external systems, `useMemo` for state transformation, `useRef` for storing stuff outside of the state you want to react to,... Then you use custom hooks to make the code modular, stuff like usePost, useProfile, useCommentUpvote,... (HN domain)
If you design your state well, the application, at least the UI layer, becomes easy to code and maintain.
What's a better alternative for shared types? OpenAPI is really outdated and the tooling is always a mess.
There is ts-rs [1], but it's only for TS.
Running rust in wasm works really well. I feel like I'm the world's biggest cheerleader for it, but I was just amazed at how well it works. The one annoying thing is using web APIs through rust - you can do it with web-sys and js-sys, but it's rarely as ergonomic as it is in javascript. I usually end up writing wrapper libraries that make it easy, sometimes even easier than javascript (e.g. in rust I can use weblocks with RAII)
It does work well logically but performance is pretty bad. I had a nontrivial Rust project running on Cloudflare Workers, and CPU time very often clocked 10-60ms per request. This is >50x what the equivalent JS worker probably would've clocked. And in that environment you pay for CPU time...
The rust-js layer can be slow. But the actual rust code is much faster than the equivalent JS in my experience. My project would not be technically possible with javascript levels of performance
That's fair and makes sense. In my case it was just a regular web app where the only reason for it being in Rust was that I like the language.
did you profile what made it so slow specifically? sounds waaaaay worse than I would expect
I did. I don't remember the specifics too well but a lot of it was cold starts. So just crunching the massive wasm binary was a big part of it. Otherwise it was the matchit library and js interop marshalling taking the rest of the time.
edit: and it cold started quite often. Even with sustained traffic from the same source it would cold start every few requests.
the JS layer is slow, indeed, but it shouldn't be that much slower that it meaningfully impacts frontend apps
A demonstration of that by the creator of Leptos:
I'm doing this now and it's mostly great but the openapi generators are not good. At least the Typescript ones produce confusing function signatures and invalid type syntax in some cases.
Why not Angilar? React and Angular are not worth comparing directly, but why not use Angular for the web interface?
Rust doesn't make sense for web development, any compiled language with automatic memory management, and value types, has much better tooling and ecosystem.
Use it where it is ideal, system programming level tasks where for whatever reasons automatic memory management is either not possible, or not wanted for various reasons.
Rust for Web is awesome for adding control interfaces etc to other programs who have a different primary purpose.
And even then I do it by serving JSON API's and not by serving HTML.
I looove Rust for the backend.
I've supported backends in typescript, python, Java, and Rust.
Rust pages me the least at night. Sleep is beautiful.
Author here.
I agree with you. Rust is rock-solid. I had zero crashes with Rust. But, having said that, I so-far have zero crashes with Node.js as well. Maybe because I'm a one man team, and I'm very pedantic, so everything is wrapped in try/catch, schema validations, and strict typescript/eslint rules.
I would agree with you that *by default*, Rust makes it harder to write bad/bug prone code compared to others, but with discipline (which big teams in "fast moving environments" usually don't have), you can get similar assurances with Node/Typescript.
Yet Cloudflare happened.
rescript [https://rescript-lang.org/] would make a nice middle ground between rust and typescript
A fan of ML, and rescript looks lovely, but sadly typescript is good enough for UI work.
Rust shines in user-space systems-level applications (databases, cloud infrastructure, etc.) but definitely feels a bit out of place in more business-logic heavy applications.
You must have meant something else because it's also great at business logic.
> And the occasional struggles with typescript where the runtime seems to be changing too often; is it ts-node? tsx? tsm? The built-in typescript runtime in node? deno? bun?
This whole paragraph is so true. The last couple of years have been pretty rough in Node land.
Rust for web backends is such a weird choice.
Use a (statically typed) GC language for that.
Well, yep. People underappreciate the Typescript/JS ecosystem.
Typescript is pretty type-safe, and it's perfectly integrated with hot code reload, debuggers, and all the usual tools. Adding transpilation in that flow only creates friction.
That's also why things like Blazor are going nowhere. C# is nicer than Typescript, but the additional friction of WASM roundtrips just eats all the advantage.
IDK, I still miss Rust's strictness and exhaustive enum matching.
I don't know about what other strictness you're referring to but exhaustive enum matching is common check in most TS stacks via eslint. Yea, it's not builtin, just saying there's a solution and it's super common.
The thing you gotta make sure to do is basically return inside of switch statements. Typescript will gladly propagate a type that you think is never but is actually ThatOneCaseYouForgot if it's not ever used or referred to.
Every TS project I've worked on has an assertNever(val) thing specifically to check for this kinda stuff
last time I researched enums in TS for a project, they were a mess such that it was better not to use enums in the first place
You can actually have it built-in (via default case in 'switch' statements having a 'never()' statement). But it's less powerful than Rust's.
Or you don't use the defualt case and rely on definite assignment analysis or checks for returns in every code path.
I find the never type in TS actually being a proper bottom type + having control-flow based types vastly superior to what rust offers.
Enum matching is one of the better aspects of Rust.
I think the big thing keeping Blazor back is that C# doesn't work well with WASM. It was built at a time when JIT-optimized languages with a larger runtime were in-vogue. That's fine in a lot of cases, but it means that C# isn't well suited for shipping a small amount of code over the wire to browsers. A Blazor payload is going to end up being over 4MB. If you use ahead of time compilation, that can balloon to 3x more. The fact that C# offers internal pointers makes it incompatible with the current WASM GC implementation.
Blazor performance is around 3x slower than React, it'll use 15-20x more RAM, and it's 20x larger over the wire. I think if Blazor could match React performance, it'd be quite popular. As it stands, it's hard to seriously consider it for something where users have other options.
Microsoft has been working to make C#/.NET better for AOT compilation, but it's tough. Java has been going through this too. I don't really know what state it's at, but (for example) when you have a lot of libraries doing runtime code generation, that's fine when you have a JIT compiler running the program. Any new code generated at runtime can be run and optimized like any other code that it's running.
People do underappreciate the JS/TS ecosystem, but I think there are other reasons holding back stuff running on WASM. With Blazor, performance, memory usage, and payload size are big issues. With Flutter and Compose Multiplatform, neither is giving you a normal HTML page and instead just renders onto a canvas. With Rust, projects like Dioxus are small and relatively new. And before WASM GC and the shared heap, there was always more overhead for anything doing DOM stuff. WASM GC is also pretty new - it's only been a little over a year since all the major browsers supported it. We're really in the infancy of other languages in the browser.
WASM is a solution looking for a problem that most people don't care.
ASP.NET MVC alongside JS/TS frameworks does the job just fine, as does Spring, Quarkus and co.
I feel like it was less than a year ago that Typescript was basically the only game in town and if you liked anything else you were a loon.
I have been an anti Typescript guy for a long time but I wouldn't deny for a moment that it's probably by far the most mature ecosystem.
This is oddly timed in as much as one of the big success stories I've heard from a friend is their new practice of having Claude Code develop in Rust, than translate that to WebAssembly.
That seems much more like the future than embracing Node... <emoji here>
If you’re making a web app your fancy rust wasm module still has to interface with the dom, so you can’t escape that. Claude might offer you some fake simplicity on that front for awhile, but skeptical that’s it fully scalable
There are plenty of Rust frameworks that handle this interface for you, including calling Rust functions from JS and JS functions from Rust.
As someone who went in the opposite direction from Node to Rust, I feel like OP is just trading one set of problems for another set of substantially worse problems. I guess the grass is always greener in the other ecosystem ¯\_(ツ)_/¯
Idk, it just feels like OP chose all the wrong approaches with Rust, including using a separate language and ecosystem for the frontend, which is where most of the friction comes from. For example, Dioxus is a React clone that is somehow leagues better than React (and Next.js, too), and it has hot-reloading that brings compiles down to subsecond times, which makes building UI with it just as productive as with Node / Vite etc. I use it for server side code as well and it's great. Compilation times can be an issue with Rust, it's something I miss from Go, but there are ways to improve on it, and just being smart about what deps you include, avoiding overuse of macros etc can make a difference. I know these things were not around when OP started using Rust for their application, but they are around now.
Node and TS are quite frankly inferior to Rust in most ways. Bad language, ecosystem full of buggy unmaintained packages with the worse security profile of all the common languages, no unified build tooling that seems to break your project every 6 months, constant churn of blessed frameworks and tools, an stdlib that is not much more comprehensive than Rust's and outright broken in some ways, at least three different approaches to modules (esm, commonjs, umd, and more...?), I could go on an on. There is a reason why everyone seemingly reinvents the wheel in that ecosystem over and over again -- the language and platform is fundamentally not capable of achieving peoples goals, and every solution developed comes with massive tradeoffs that the next iteration attempts to solve, but that just creates additional issues or regressions for future attempts to tackle.
I've been using Rust with Dioxus and was completely mind blown when I started with it. With barely knowing any Rust (just React) I was able to jump right in and build with it, somehow it was more intuitive to me than most modern JS full stack frameworks. It seemingly already has most if not all of the features that similar JS frameworks have been developing for years, and because it's written in Rust things like conditional compilation are built into the language instead of being a third party babel plugin. That helps to remove a ton of friction. And it's trivial to build those same apps for desktop and mobile as well, something that's basically not possible with the JS frameworks.
Even stuff like websockets, go try to implement a type safe web socket connection with a server and client in Next.js or Astro. You'll need a ws library, something like Zod for validation, etc. In Rust it's just:
#[derive(Serialize, Deserialize, Clone, Default)]
enum SocketMessage { Hello(id: i32) }
#[get("/api/ws")]
async fn web_socket(options: WebSocketOptions) -> Websocket<SocketMessage> {
options.on_upgrade(move |mut socket| async move {
while let Ok(msg) = socket.recv().await {
match msg { SocketMessage::Hello(id) => {} } // handle messages
}
})
}
fn App() -> Component {
let mut socket = use_websocket(web_socket);
rsx!{ button { onclick: move || socket.send(SocketMessage::Hello(42), "say hello" } }
}I think this is spot on. I've used Iced and Dioxus and both are great. I do take the author's point that the actual UI code, even in Dioxus, is verbose. It is. And that's a trade off I'm willing to make for guaranteed correctness.
I haven't used Iced but re. Dioxus, I don't know if it's necessary more verbose conceptually. One of the most frustrating things with React is handling async updates, and while Rust's async story is conceptually difficult, it's ultimately much easier to reason about (imo). Like are we sure a comparable component in React would be any less verbose?
Imo the RSX here is much less verbose than JSX. Inline match statement, inline if statement, inline for loop, .take(3) compared to `Array.from({ length: 3 }).map((_, i) => urls[i]))`, etc etc. This gives you automatic cancellation of the future, whereas with React you would need a third party library like React Query, and then manually abort requests in the asynchronous function with an abort signal -- in Rust, you get that for free. You also get data validation for free, instead of needing eg. Zod for manual runtime validation.let mut breed = use_signal(|| "hound".to_string()); let dogs = use_resource(move || async move { reqwest::Client::new() .get(format!("https://dog.ceo/api/breed/{breed}/images")) .send() .await? .json::<BreedResponse>() .await }); rsx! { input { value: "{breed}", oninput: move |e| breed.set(e.value()), } div { display: "flex", flex_direction: "row", if let Some(response) = &*dogs.read() { match response { Ok(urls) => rsx! { for image in urls.iter().take(3) { img { src: "{image}", width: "100px", height: "100px", } } }, Err(err) => rsx! { "Failed to fetch response: {err}" }, } } else { "Loading..." } } }
I run Rust/Axum for something that's less a web app and more a living system — autonomous agents, real-time state (pulse, mood, health metrics), a frontend that shifts based on internal conditions rather than just user input.
For this kind of use case, Rust's type system isn't overhead — it's the reason things stay coherent when multiple agents are running concurrently. The error handling that feels like boilerplate in a CRUD app actually matters when a missed unwrap means your system silently stops working at 3am.
The article reads to me like someone building a content site in Rust, which yeah, is painful for no reason. But I'd push back on the broader framing — the web is more than blogs and dashboards. When your backend does meaningful autonomous work beyond serializing JSON, Rust pays for itself.
This comment sounds like an AI wrote it.
Welcome to MoltNews. Can’t escape the slop.