My Web Stack For 2022 - Rails + Inertia + Vue3 + Tailwind

1st January 2022 – 1758 words

2021 was an mixed year. On one side, we have all experienced the 2nd year of a pandemic which we are not sure, how many waves are still in front of us. But tech did not stood still. New frameworks are popping up, browser compatibility gets better and better, And Ruby on Rails recently released the new version 7 with new Ruby version 3.0.

At the moment, I am in the final phase of building a new job board, which we will release soon which uses most of the stack below (except GraphQL). It will be a Remote-Only Job-Board for the German market. It was a joy to develop for now and I my confidence is growing with this stack. This is why I want to use this post to describe my “dream stack” for building off-the-shelf Web-Apps.

Backend: Rails 7

After 10 years of using Rails, it still gets better and better with every release. The ‘7 has some really great additions, like Attribute Encryption built-in, and a new simplified integration with common frontend libraries which now do not necessarily need Webpack(er). On the other hand, Rails now offers a solution for building more interactive “CRUD” apps by using the Hotwire Stimulus/Turbo combo. But I have to admit, I was never a big fan of Turbolinks before, and always reverted Stimulus development spikes. On the other hand, I have no big fear from Javascript frontends. But as DHH said, Rails aims to offer a “big tent” were people with different technology biases can still agree on a couple of core components and thus delivery immense value when integrating 3rd party libraries.

Were I usually diverge from Vanilla/”Omakase” Rails:

  • ActionView: SLIM, View-Components or Inertia (see later about that) instead of ERB / partials
  • Turbo/Turbolinks: Turbo wants to encompass the whole app / “All-In” approach, integration with other JS libraries not always easy, so I usually skip Turbo(links)
  • Stimulus: I totally not like to sprinkle my views with magic attributes that only maps via “magic” and so couple them strictly without any validations by pure naming to some Javascript files somewhere (Always wondering: Was it camelcase? kebab-case? Why did the component not load? Why is the value empty? Whats the name of the value? And no good OOTB Typescript support (you have to manually describe the fieldValue’s instead of just describing a State object or similar). I generally followed the “Mounting” approach of bootstrapping small Vue “Apps” all over the app with a set of props. But this step is now redundant when using Inertia instead.
  • RSpec instead of Test-Unit: RSpec’s test runner was always very good, next-failure/only-failure/bisect are all powerful tools. I never use too much RSpec magic (no subject or its, mostly only == matchers). On the other hand, I now envy the Test-Unit’s built-in parallelization.
  • ActionText: The Wysiwyg editor Trix is IMO a poor choice for many apps, as it is hard to customize and the OOTB experience is a little to underpowered. Most of the times we use either TinyMCE, when we need a full editor with all bells & whistles (even support like Tables, “Paste from Word” etc.), TipTap, if you need something totally customized for your app, Editor.js for blocky like standardized content or Quill as a Good enough replacement for a standard Wysiwyg
  • ActionMailbox: Sounds interesting, but unfortunately, for our prime use case, our ATS (Applicant Tracking System) it was released after we build our own system for handling inbound mails.

So the remaining parts, namely ActiveRecord + Migrations, ActiveSupport, ActionController, ActionMailer, ActiveStorage, and even ActiveJob are used extensively.

Database: Relational Database / PostgreSQL

PG is a very good database, and a traditional ACID-compliant relational database a very good choice for most use cases. I especially like PostgreSQLs various column types, like jsonb, and I occasionally used CTE and Window Functions. Surprisingly, one of the best productivity features for our small team is that even DDL statements (Alter table/create/drop table etc.) are wrapped in a transaction. With one of our apps that is still on MySQL, it is always a pain to write longer migrations with multiple steps were the last line fails and you are now in a bad in-between-state where you have to manually drop tables or comment out + rollback migrations. With PG that can not happens and you can now easily write larger migrations were you add a table + foreign keys and migrate some data at the same time atomically.

I even successfully used text search with PostgreSQL and replaced one of our Elasticsearch job search features by implementing many of the features which we used Elasticsearch for (synonyms, predictable scoring, ranking, highlighting, stemming, partial search, facets). We still occasionally use Redis as a Cache store, or as the backend for the async job queue, but almost never use it directly.

Backend API: GraphQL

When I want to built a new API for a app with more than a couple models and with many attributes, I really come to like GraphQL now. If you already have a well implemented schema it’s a bless to just add a mutation or query type and it’s easy to extend.

The advantages are many:

  • built-in type-safe API with introspection features,
  • big ecosystem and tooling + client libraries
  • Ability, to generate typescript typings and typed API Clients through graphl schema generator

The only disadvantage is in my opinion the that HTTP logging is more hard to setup, as all logs are just POST /graphql instead of a REST+path. That makes filtering in a standard logging interface more difficult.

Backend to Frontend layer: Inertia.js

That’s maybe a surprise: In the last couple of month I grow to like Inertia.js’s approach, which is glueing together classic server-side frameworks (Rails + Laraval) with front-end Frameworks (Vue/React/Svelte). In a way, Inertia replaces your Rails-Views completely and instead you “call” your Components directly from the controller and pass required props. Inertia then takes care of loading the component on the first page load with a classic “mount” script. Inertia also replaces the client side routing, like vue-router/react-router. On navigation events, it instead calls your Rails routes/controller and then replaced the component + props, which it loads asynchronously via fetch. In general it’s a very simple approach which standardizes the component mounting glue-code and extra respond_to { |f| f.json ... calls, which otherwise accumulated. I also dabbled around with Server Side Rendering which helps your Web-Vitals or clients without Javascript. I will try this out later next year, and probably write in this Blog if I had some experiences

Component Framework: Vue 3

After a slow start with Vue 3, I now like the new iteration of the framework. Especially, after the LSP-Support through Volar got much better now, and with the new obviously by Svelte inspired “script setup” syntax, it’s much more enjoyable to create components with less boilerplate and strong typings. Until now, I didn’t had to use a Store package (like Pinia), but solely rely on simple store files which just exports a couple of refs.

Language: Typescript

After Neovim now has better and better LSP-Support, Typescript (+ Volar) autocompletions are so incredibale useful, that I switch everything over to Typescript. Combining it with GraphQL schema generator or similar, it is possible to generate much more safe Javascript UI’s. Typescript is very well thought out language and has very good tooling now.

After having now a good editor support, I also dropped ESlint, as the Typescript warnings + Prettier are good enough for me.

CSS: Tailwind (UI)

Coming a little late to the party, I now understand the appeal of Tailwind after I first turned away in disgust. I think it does make most sense if you are componentizing your app with something like View-Components or Vue/React. In the past I used Bootstrap 4 extensively and more and more relied on the helper-classes which were built-in (m-*, d-flex, text-center etc.), but it was always missing a lot of things. Starting with Tailwind is frankely a little more exhausing than just adding Bootstrap, as you will need to define your base components (Button, Card, etc.) yourself or use a layer like Daisy UI.

Tailwind UI + HeadlessUI is a good collection of example JS which helps building out the parts of the App which I not necessarily care so much about and just want to have a starter template (Navbar, Landing-Pages, Pricing Pages, etc.). Tailwind UI is especially valuable, because it has Vue/React templates for each example, so it is even faster to get started by just copy & paste & adjusting

Bundler: Vite

We have been using Webpack with the @rails/webpacker variant the last couple of years. It was always good to use and very valuable, but as most people experience, upgrades between major versions are not always easy, there is a lot of “Lingo” like Loader, Plugin, Entrypoint and the build gets slower and slower. After Rails 7, you can now choose to use a “dumb” bundler via jsbundling asset pipeline. But if you want to use Hot Module Reloading or load all kinds of assets, then you might still be stuck with Webpacker, or do you? I recently discovered Vite which also has a great Rails-integration and follows the way and scope of @rails/webpacker. Out of the box, it supports common scenarios like Typescript, PostCSS/Scss and has plugins for integrating Vue and similar. Compared to Webpack, the config is minimal (like 10 lines in total or so), it’s much faster and mostly just worked fine for us. In our case, we are usually working on a remote server proxied behind a developer-domain, and Vite could be easily configured via Env-variables to pick up a different http-host/port combo.

IDE: Nvim with LSP Support + remote Tmux

My preferred working environment is our long-running beefy development server, which has has a TMux session for each project opened at any time. Each project (Tmux session) has about 3-8 shell windows with shell, development server, bundler, background job etc. and one Neovim window. 2021 was a great year for Neovim. A lot of plugins have been developed, and Language-Server-Protocol (LSP) support is now much more enhanced, as well as syntax highlighting gets faster and more stable through Treesitter. So it is now easier than before, to make use of the VSCode (and Atom)-ecosystem and to use language support for Vue or Typescript through Volar / TS-Server.