Setup Modern Client-Side Rails | TailwindCSS + TypeScript

Patryk RogalaPatryk Rogala
6 min read

Most websites today use special features to make them more enjoyable for users. Although Rails is mainly used on the server side, it has always had tools to help send code to the user’s browser.

Making it easy to work on the user side has always been one of the advantages of Rails. Over time, the user-side tools have become stronger, which is good, but they have also become more complicated, which can pose some challenges.

However, one thing that hasn’t changed is that Rails still has its own way of dealing with these user-side tools.

Let’s create a simple step-by-step template for creating modern Ruby on Rails applications with Tailwind and Typescript.

Setting up new Rails project

In this example i will use Ruby 3.2.2 and Node 20.10.0.

I prefer ASDF as a runtime environment version manager that allows me to create a simple .tool-versions file in the project folder.

# .tool-versions

ruby 3.2.2
nodejs 20.10.0

To install and setup my tool versions:

asdf install

Let’s create our new Rails application.

rails new . -a propshaft -j esbuild --database postgresql --skip-test --css tailwind
  • -a propshaft —asset pipeline library for Rails (instead of Sprockets)

  • -j esbuild — javascript building library (instead of importmap)

  • — database postgresql — use PostgreSQL database instead of SQLite

  • — skip-test — skip default test library (just install Rspec)

  • — css tailwind — Tailwind CLI for CSS packaging (instead of nothing)

With these options, you get a few gems installed, some manifest and configuration files, and a startup script to load everything.

Running the application

Rails created a startup script for us bin/dev which uses foreman to run our application with config from Procfile.dev

# Procfile.dev

web: env RUBY_DEBUG_OPEN=true bin/rails server
js: yarn build --watch
css: yarn build:css --watch

Each line of the Procfile is a separate process.

We’re starting three processes:

  • The Rails server itself

  • The yarn build command, which triggers esbuild to bundle our JavaScript. This command uses the — watch parameter, so it will run in the background when a file changes.

  • The yarn build:css command, which triggers Tailwind to bundle our CSS. This command also uses the — watch parameter to run in the background.

Adding TypeScript to our App

Esbuild can change TypeScript (.ts) files into JavaScript without us needing to change any settings. But, here’s the not-so-good part: esbuild only removes the special things that are just for TypeScript, like type annotations. It doesn’t check if the code is safe according to TypeScript rules. We actually want TypeScript to check if our code is safe, so this situation doesn’t seem perfect for what we need.

How can we solve this problem? Well, by installing some additional packages.

yarn add --dev typescript tsc-watch

Here we are installing:

  • TypeScript itself

  • tsc-watch which starts the installed TypeScript compiler with watch parametere

Configure TypeScript

Let’s create a tsconfig.json file which we will use to configure TypeScript behavior.

{
  "compilerOptions": {
    "declaration": false, // Don't generate .d.ts files for every TypeScript or JavaScript file inside your project. 
    "emitDecoratorMetadata": true, // Enables experimental support for emitting type metadata for decorators 
    "experimentalDecorators": true, // Enables experimental support for decorators, which is a version of decorators that predates the TC39 standardization process.
    "lib": ["es2019", "dom"], // Includes a default set of type definitions for built-in JS APIs
    "jsx": "react", // Controls how JSX constructs are emitted in JavaScript files.
    "module": "es6", // Sets the module system for the program
    "moduleResolution": "node", // Specify the module resolution strategy
    "baseUrl": ".", // Sets a base directory from which to resolve bare specifier module names
    "paths": { // A series of entries which re-map imports to lookup locations relative to the baseUrl
      "*": ["node_modules/*", "app/packs/*"]
    },
    "sourceMap": true, // Enables the generation of sourcemap files. These files allow debuggers and other tools to display the original TypeScript source code when actually working with the emitted JavaScript files.
    "target": "es2019", // The target setting changes which JS features are downleveled and which are left intact.
    "noEmit": true // Do not emit compiler output files like JavaScript source code, source-maps or declarations.
  },
  "exclude": ["**/*.spec.ts", "node_modules", "vendor", "public"], // Specifies an array of filenames or patterns that should be skipped when resolving include.
  "compileOnSave": false
}

Update scripts

Now we need one more thing to make it work!

We need to update our scripts in package.json to make use of our newly added TypeScript.

// package.json
"scripts": {
  "build:js": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets", // Here we update name from builds to builds:js to match with build:css
  "build:css": "tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css --minify",
  "failure:js": "rm ./app/assets/builds/application.js && rm ./app/assets/builds/application.js.map", // If something went wrong, delete compiled JS
  "dev": "tsc-watch --noClear -p tsconfig.json  --onSuccess \"yarn build:js\" --onFailure \"yarn failure:js\"" // Run tsc-watch with our config
},

I’m calling tsc-watch with four arguments:

  • noClear which prevents tsc-watch from clearing the console window.

  • -p tsconfig.json which points to the TypeScript configuration file

  • — onSuccess \”yarn build:js\” which controls what you want to have happen if the TypeScript compilation succeeds. In our case, we want the regular esbuild build:js to happen, since we now know the code is type-safe.

  • — onFailure \”yarn failure:js\”, which controls what you want to have happen if the TypeScript compilation fails. We remove the existing esbuild files from the build directory so that the development browser page will error rather than return the most recent successful compilations.

Now we need to update our Procfile for js process to use our newly added dev script

# Profile.dev

web: env RUBY_DEBUG_OPEN=true bin/rails server
js: yarn dev # <--- HERE WE UPDATE COMMAND
css: yarn build:css --watch

And that’s it, let’s run bin/dev script and see our application running.

15:36:03 js.1   | error TS18003: No inputs were found in config file '<PATH>'. Specified 'include' paths were '["**/*"]' and 'exclude' paths were '["**/*.spec.ts","node_modules","vendor","public"]'.
15:36:03 js.1   |
15:36:03 js.1   | 3:36:03 PM - Found 1 error. Watching for file changes.

And we received an error… What did we do wrong?

Nothing. The problem is that we don’t have .ts files for TSC to compile, this won’t be a problem when we start adding our TypeScript files, but for now let’s solve this problem by renaming hello_controller.js to hello_controller.ts .

Let’s try once again

15:39:07 web.1  | * Listening on http://127.0.0.1:3000
15:39:07 web.1  | * Listening on http://[::1]:3000
15:39:07 web.1  | Use Ctrl-C to stop
15:39:07 css.1  |
15:39:07 css.1  | Done in 343ms.
15:39:07 js.1   |
15:39:07 js.1   | 3:39:07 PM - Found 0 errors. Watching for file changes.
15:39:07 js.1   | $ esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets

And here we are! Ready to build our new modern Rails application.

0
Subscribe to my newsletter

Read articles from Patryk Rogala directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Patryk Rogala
Patryk Rogala