Rails Is Good Enough
onurozer.me
onurozer.me
This is possibly the absolute worst combination of factors possible. The learning curve makes it bad to ramp up with, and the magic makes it bad in the long term.
It's a framework that wants to be used for monoliths, and tooling is essential to manage the complexity of large monoliths, but you're stuck with a language that's extremely hostile to tooling, using patterns that make it even more hostile to tooling.
So, really, what you're left with is a framework that's only actually good for the early-mid stages of a project, and only if you're an existing Rails user who doesn't have to pay the ramp up price.
Rails has an incredibly convenient REPL that's built into every dev error page, a one-line integration with every error, analytics, and log-tracking service under the sun, that will give you all of the insight into production runtime for basically zero effort.
You install Sidekiq or GoodJob in a couple of lines of code, and suddenly you have the entire background job system to tap into for any delayed or periodic workflows that can be offloaded, bundled with monitoring and administration UI.
All you need to do is learn how to do software design, but even there Rails starts you off with arguably the best design decisions you can make at early stage: focus on your entry points (controller actions for requests, rake tasks for CLI), and start extracting domain objects as you learn more about your domain.
Am I missing something? Where's the "hostility to tooling"?
You can either have a massive Python project and struggle with a lack of types and a tenseness that becomes unreadable at huge sizes;
A Laravel project that’s PHP, which despite being way, way, better than it used to be (types! traits! union type returns! faster than ruby! weak maps! [0]) still has a few footguns left;
Or you can have a JavaScript monster. The worst of all worlds. Your complexity complaints times 10. A nightmare than in 10 years will make a 2008 PHP project look well-organized. A project that has thousands of packages you’ve never read.
On the edges, you’ve got microservices (which turns into a mess); or you can go fancy with Go and Rust (have fun managing the budget). There’s also, of course, C# and Java, but come now, it’s 2024, you’re probably coupling them to a JavaScript monster anyway.
TL;DR: Every large project becomes a mess. Pick your poison. One is hardly worse than another’s. Best to have the devil you know. Even if you do things perfectly with a FAANG level budget, AWS and Azure still have outages.
I've tried many stacks in the last 6 months, including Rails. Too many outdated gems, too much magic, generators from gems doing who knows what with undo only possible because of Git.
A lot of these posts like in the OP are written by dinosaurs [1] and I mean no offence, at some point you have to give modern stack a chance, the developer UX with hot reloading that works without hacks and plethora of ready to use components in React world is just great.
1. https://peterxjang.com/blog/modern-javascript-explained-for-... this was written in 2017 but is still relevant, shows how much the ecosystem stabilised.
I am not proficient in Go, but I assume that you will go in two directions: 1. Either write the code for it yourself 2. Install a package
Here is where Rails shines: it comes with a battery included so for example, session management is as simple as doing:
session[:user_id] = user.id
User.find(session[:user_id])
I don't need to write my own code to handle this basic feature of a web app not do I have to decide what package to use.Regarding the "modern" tech stack: What is so modern about Go + Templ + htmx?
Rails can work with htmx and has a wide range of templating options for HTML.
And if modern means static typing, I have bad news for you: types are old.
I don't want to say that GoTH is not a good stack. It could be a good stack, but choosing it because it is more modern than Rails is not the real reason.
> Too many outdated gems, too much magic, generators from gems doing who knows what with undo only possible because of Git
What gems have you tried? For building an API with Rails if that is what you can go a long way with Rails in API only mode.
Regarding ORM, I've grown to dislike them eventually, even though I've used gORM which is very popular in Go land. Currently my SQL queries are handled by sqlc (example) [4] which compile the queries down to type safe Go code at build time.
Types are old of course, and I didn't mean modern = GoTH or static typing, I guess modern in my comment was for node based frontend dev with React/Astro, I won't expand on this in this comment, it's getting long as is.
Strong static typing is ridiculously good once you develop with it for a while, editor support and guarantees you get in the sense "if it compiles, it likely works" begin paying dividends soon after the project grows a bit. Not Rust level, you can write weakly typed Go if you want but that's not the default. I have type guarantees even through frontend/backend or backend-backend boundaries with gRPC/connect without writing any marshal/unmarshal code myself.
Regarding outdated gems, I can't recall now, I tried looking up my history but didn't really find anything specific. I started with barebones with devise for auth and some redcarpet markdown parsing. I had troubles setting up hot reloading, AFAIK it should work OOB with Rails 7 but didn't, then I tried hotwire-livereload [5] which pulled in Redis but also worked sort of janky. Contrast that to how great Vite based project live reloads work for modern frontend or how simple and fast Go backends live reload with Air [6].
To end, if Rails works for you and you know Ruby already then it's a great stack, I didn't like certain things and magic and view Ruby as a dead end skill career wise so in the end I ended up not using it. I use Go daily at my dayjob so it makes sense to just use what I already know.
1. https://blog.cloudflare.com/the-complete-guide-to-golang-net...
2. https://www.digitalocean.com/community/tutorials/how-to-use-...
3. https://go.dev/blog/routing-enhancements
4. https://docs.sqlc.dev/en/latest/tutorials/getting-started-po...
It just feels so much more productive than anything else I’ve ever worked with.
So what are your options... Sorbet? Does this feel like Ruby?
sig do
params(x: Integer)
.returns(String)
end
How about RBS? Does writing the equivalent of header files for every function and class feel like Ruby?So unfortunately I'm no longer an evangelist or practitioner.
I love the syntax and elegance of Ruby, but it really is maddening how difficult it is to hold all the abstractions and state in my head without the mental boundaries that good type support provides.
I often commiserate with a coworker who came from a Java background (and I from Swift/Python) about how frustrating it is when type-based issues leak to prod and cause trouble, or how it lengthens the development process insofar as determining what a given object _is_ at any point in the logical execution.
I really understand how unit testing became so popular, given Rails's historical popularity: there's simply no other way to move with confidence through the codebase without a plethora of specific tests for each individual component.
Say you want to understand what does `belongs_to` do?
Just open `rails console` and you can do:
User.method(:belongs_to).source_location
And opening the file that is printed at the specified line, you can discover the following code: def belongs_to(name, scope = nil, **options)
reflection = Builder::BelongsTo.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
end
And moreso that method has a very good comment before the declaration, documenting everything it doesYou can also try `show_source` command in Rails console if you have the debug gem installed:
show_source User.find
And this will directly show the source code of the `find` method.I do agree that for methods defined with meta-programming it will be harder to find the methods.
But again here rails console comes with help:
For example if you do:
User.method(:find_by_name)
=>
#<Method: User(id: integer, nickname: string, name: string, created_at: datetime, updated_at: datetime).find_by_name(_name) <edited by me>/vendor/bundle/ruby/3.2.0/gems/activerecord-7.0.8.4/lib/active_record/dynamic_matchers.rb:65>
Of course, this is mode complex meta-programming code, but you can still go to it, read it, and see what it does.In case of relationships between entities in the code, you can try Sorbet. It is pretty good these days.
At this point I always have a rails console up, just to be able to poke at objects and whatnot, but it feels... clunky. Similarly, when designing system changes I first go through with breakpoints in an existing flow to see what objects and behavior actually are when invoked, because I can't tell just from reading the code half the time--and that drives me bananas.
sig { params(x: Integer).returns(String) }
I can read it outloud:
Signature is defined as: one parameter called x of type Integer and will return a String.Sorbet may have some warts, but the example you show is simple and easy to understand.
The `sig` is as Ruby as you get IMO:
class MyTest
extend T::Sig
end
MyTest.method(:sig).source_location
=> ["...gem/ruby/3.3.4/gems/sorbet-runtime-0.5.11495/lib/types/sig.rb", 27]
And when opening that file we can get something like: def sig(arg0=nil, &blk)
T::Private::Methods.declare_sig(self, Kernel.caller_locations(1, 1)&.first, arg0, &blk)
end
I do agree about RBS being separate files that is something that can stop a coding flow.RubyMine is great.
He’s using the built in rails front end templates which React can replace trivially in a React+Rails stack and comparing React to all of the rails stack and complaining it can’t do backend which is not where React fits in.
It’s a cringeworthy post. It should not be on the front page.
FWIW, the javascript side of Rails is historically the least settled-upon part of the framework. I'm curious how many current Rails projects use React as an alternative over the current "convention" of Turbo. Or Hotwire. Or even UJS for legacy projects.
Plus, Rails story has no clear JSON boundary between back and front, which creates some discomfort (i.e. learning erb) as well. As a backend dev, I'm all-in on Rails in solo projects.
You can use node to bundle react into a minified js bundle. But that’s typical for any complex js usage. My raw rails apps still have a node step to bundle a bunch of js libraries I need for the front end.
I honestly think the post is just incredibly naive and they haven’t ever needed more than basic rails templated html functionality in their frontend. They can’t see the value in a larger frontend framework because they don’t need it. They tried replacing the entire rails stack with a front end and didn’t understand why anyone would use that and found it painful to do the backend stuff.
Here’s the path for the og poster to grow. Add more and more complexity to the frontend. At some point you’ll want to use the node system to manage the front end js libraries you are using. At some point you’ll want something more consistent to manage the front end across the site. You’ll look to Vue or Angular or React. You add this to your rails app and now you have a very common rails+react stack.
It can’t see a justification for this post apart from naivety.
However, there's another path, which is where you keep things on the backend. Unless you're talking about very complex interactive experiences, the amount of front-end code can go from >100% of the backend code, to <5-10%. Backend always implements almost everything anyway, and the question is how much should front-end reimplement on their side too.
Every time I push for smaller front-end, I'm faced with how the team might get bored or not have enough to show on the resume.
I love Rails. Every investment I've made in learning it has yielded returns for years. Everytime I learn a new JS framework, its obsolete two years later.
Model tests, controller tests, integration tests, system tests. Its got it all. Just run 'rails test'
ActiveRecord has many limitations when trying to get efficiency from infrastructure (either because you've grown/scaled, or to minimize costs while growing). The main problem is with model instances having actions attached to them, you have to make and jump through hoops to batch the individual callbacks.
Working on a performance improvement project, we discovered that doing ModelClass.new (not doing any I/O and even if that class is empty with no callbacks) is about 400x slower than doing MyClass.new for a plain Ruby class that holds a table's attributes. Processing a thousand of these things in a request with a handful of associated models for each can eat up 100s of ms before doing anything useful.
And even dropping down to using ModelClass.insert_all(array_of_hashes) which bypasses ActiveRecord model instantiation is still slow in the way it generates the SQL before executing it.
You can scale throughput by having many webapp servers (at the cost/constraint of many database connections) but that doesn't help latency per request.
Working at a Rails and MySQL shop is about as close to guaranteed employment as I know as there's always tough problems to solve as a company continues to expand in scope and scale, and always new devs doing their best but still introducing patterns that don't age well.
That's absolutely meant as a compliment!
However, convention over configuration means quicker delivery for small(er) teams, but more discussions once more opinionated developers join.
If an opinionated developer thinks a different configuration is better, then that helps form the conversation immediately. Everyone is already familiar with the convention, so the discussion only needs to inform everyone about the pros/cons of the alternative.
For example, it promotes use of concerns, which can easily go out of control. Then other people come with singletons, interactors or service objects.
Or, like Minitest, which is fine to begin with, but the sooner you switch to RSpec, you will be better off in the long run.
In my experience, these kind of tooling options tend to fire up almost religious discussions.
1. Code is loaded automatically and globally. I’ve had weird bugs caused by someone creating a function with the same name in a completely different file.
2. The module system is strange. Why do my file names and class names need to match? Why can’t i just import the files i want like other languages?
3. Includes (aka mixins) are a bad idea. See below.
4. Many of the language features are designed to hide how code works. Quite often i wonder to myself “where does this variable/function come from?” #1 is also a good example of this.
5. Coming from a typed language, the lack of types makes me feel naked and more prone to screw something up.
6. The strange and elitist opinions of ruby fanboys. Sorry but code terseness is not a panacea and “unless” is terrible. It seems like code readability and understandability are less important to this crowd.
Ruby doesn't have functions. It only has Objects, and methods on those Objects. Generally you won't run into method nuking because generally there's no need to define the same class in different files.
Yes, you can overwrite method definitions on objects, you can even do that dynamically at runtime. Yes, it's a shotgun. No, it's not wrong, it's just another tool in your toolbelt.
> 2. The module system is strange. Why do my file names and class names need to match? Why can’t i just import the files i want like other languages?
You can require whatever you want, named whatever you want. Rails only looks like magic in this regard because it's coded around conventions so that it can abstract away all that boilerplate. Somewhere before you try instantiate MyController, it was require'd into the project for you utilizing those basic language features. `config/application.rb` is a good reading point to start exploring how this works.
A tremendious amount of logic in Rails is derived from how you name things. Don't fight this system, learn it. Fighting it will only be pain and agony, and I promise you there is a reason for why it works the way it does.
There is absolutly nothing stopping you from calling `require 'my_module'` to bring "BobsGreatestModuleEver" into context. You can missmatch all you want. I have no idea _why_ you would want to do that, seems like a bad precident, but go wild. Ruby works this way like any other language.
> 3. Includes (aka mixins) are a bad idea. See below. > 4. Many of the language features are designed to hide how code works. Quite often i wonder to myself “where does this variable/function come from?” #1 is also a good example of this.
Includes/Mixins are designed to encapsulate shared functionality. Like any effective shotgun if you shoot your feet off you'll be hurting, so don't shoot your feet.
Use your console! If you've got a class with a method and you have no f'n idea where it's defined, just ask it to tell you.
irb(main):002:0> MyWeirdClass.new.method(:search).source_location
=> ["/app/helpers/weird_stuff/search_methods.rb", 4]
> 5. Coming from a typed language, the lack of types makes me feel naked and more prone to screw something up.Eh, I think this is one of those tech religious war topics. I've wrangled a lot of typescript and I cannot say that having types have made me feel protected in any way. If anything, TS in particular, it's felt like just _more code_, and I'm a fan of not having any more code that can be helped.
That aside, the Ruby way is Duck typing. If your code is in danger of receiving arbitrary properties, it's very straightforward to protect that code by just inspecting the properties. If it walks like a duck and quacks like a duck, it's probably a duck.
def dangerous_method(arbitrary_thing)
return unless arbitrary_thing.is_a? KnownThing
raise UnsupportedPropertyError unless arbitrary_thing.respond_to? :each
arbitrary_thing.map { |d| ... }
end
> 6. The strange and elitist opinions of ruby fanboys. Sorry but code terseness is not a panacea and “unless” is terrible. It seems like code readability and understandability are less important to this crowd.Ruby is _all about_ code readability and developer experience. `return unless some_condition?` is gods greatest gift to programming and I will die on that hill.
It can go to an extreme. Like every language you will meet your code golfers who want to take things way, way too far, but that's not unique to Ruby. That's just a people thing. During PR review it's not abnormal for me to ask for terse code to be expanded into a longer form simply for readability.
> Ruby doesn't have functions. It only has Objects, and methods on those Objects. Generally you won't run into method nuking because generally there's no need to define the same class in different files.
I was writing a rake task and all I did was write a function above the rake task definition. That function was loaded globally and conflicted with another function. If it’s in a rake file then what object is it a part of? This goes back to my main issue about how you don’t know where shit comes from in ruby and there’s too much implicit magic.
> Rails only looks like magic in this regard because it's coded around conventions so that it can abstract away all that boilerplate
This is what I’m talking about. What you call “abstract away all the boilerplate” I call magical bullshit that makes code confusing.
> Includes/Mixins are designed to encapsulate shared functionality. Like any effective shotgun if you shoot your feet off you'll be hurting, so don't shoot your feet.
Classes already do that. I’d much rather create an instance of a helper object than magically inject methods into my class. It makes the code more explicit and discoverable too.
> I've wrangled a lot of typescript and I cannot say that having types have made me feel protected in any way.
That’s strange because types tell you what a thing is and helps you find where it comes from. It offers protection if you use the type system but to each their own.
> `return unless some_condition?` is gods greatest gift to programming and I will die on that hill.
No, just no. `return if !some_condition?` is far more readable, especially if you have other lines that use `if`. Consistency makes code more understandable. And using `if` everywhere makes it so I don’t have to invert things in my head.
> Like every language you will meet your code golfers
I’ve worked with a lot of programming languages and ruby is particularly bad. Lots of people jerking themselves off about how short their functions are and you don’t see that in other communities as much.
Maybe it is easier to understand the condition or the boolean logic. I could agree with that.
But readable? No. It is very easy to miss the `!` when reading multiple lines of code. While missing `unless` it way hardwer.
Compare:
return if organisation_exists?
return if team_exists?
return if !user_exists?
return if project_exists?
with: return if organisation_exists?
return if team_exists?
return unless user_exists?
return if project_exists?
Where can you see quicker that it will exit when the user does not exists? For me I need to read twice the lines that only have `if` and I can pick at the first read the `unless`.Not saying you should use `unless` but it is a keyword, like any other keyword that you learn when you learn a new language. We should not confuse lack of familiarity with readability.
My assertion is that lack of familiarity (or better said lack of seeing unless used often) is the cause of the hard to understand issue and not that unless is a word hard to understand on its own.
Here is a question to reflect:
How come ! is easy to understand while actually "not" is better (short enough to not consume reading speed while being explicit)? But a lot of languages are using ! so it seems to be good enough for reading.
My answer to this is: because it is used so much => familiarity.
You added a method to the base `Object` object. Everything is an Object in Ruby. Unlike Javascript, blocks in Ruby (of which Rake tasks are comprised entirely of) do not create a private context in which to hide functions. If you redefined that same function name later in the task, it would clobber your original implementation.
> Classes already do that. I’d much rather create an instance of a helper object than magically inject methods into my class. It makes the code more explicit and discoverable too.
You might even want to create a helper class, and include several modules by which to build out the functionality of that helper class. That class might even inherit from a baser class, providing even more functionality. All options are available to you in Ruby.
> To each their own, but frankly your assertions sound like nonsense to me.
It sounds like you didn't bother to learn much about the language, or framework, at all. To each their own indeed.