1. Introduction
Rails applications typically deliver server-rendered HTML to the browser. In this setup, JavaScript is used mainly to add a layer of user interactivity over the HTML document.
JavaScript files can be delivered individually using an import map, or bundled together and shipped as a single file. This guide will cover the pros and cons of each approach, as well as Rails' default JavaScript stack composed of Turbo and Stimulus (which are part of the Hotwire suite).
Rails can also be used in API-mode where it speaks JSON or XML, but in such a setup, the front-end JavaScript application is usually transmitted independently from the Rails app. As such, this guide covers the usage of JavaScript for server-rendered applications only.
Modern JavaScript has a standard structure for packaging called ECMAScript modules (ESM). Rails provides two mechanisms to deliver JavaScript written for ESM: an import map, or a JavaScript bundler.
Legacy JavaScript applications may use another structure such as
CommonJS. In such setups, JavaScript
needs to be compiled and bundled into a single file before transmission to the browser.
Common bundlers for this approach include Babel
and Webpack, but Rails doesn't integrate tightly with these
tools. You can, however, deliver pre-built or bespoke JavaScript files
using Rails.
Rails uses the Asset Pipeline to deliver all assets, including JavaScript files, to the browser.
2. Using a JavaScript Import Map
A JavaScript import map allows JavaScript files to be delivered separately without bundling and still be able to reference each other.
It is a JSON object which defines the mapping between the module specifier
passed to an import statement, and the path to the actual file to be imported.
Here's an example:
<script type="importmap" data-turbo-track="reload">
{
"imports": {
"animations": "/assets/scripts/animations.js",
"utilities": "/assets/scripts/utilities.js"
}
}
</script>
Scripts can now invoke import "animations" or import "utilities" and the
browser will import the corresponding file and the module it contains.
In Rails, the import map is constructed using the importmap-rails gem.
This technique doesn't require an additional build step for your JavaScript. Your files are delivered as-is, and hence no JavaScript runtime such as Node.js is required.
2.1. Installing importmap-rails
The importmap-rails gem is included by default in all new Rails applications.
In older applications, you can install it using:
$ bundle add importmap-rails
$ bin/rails importmap:install
2.2. Declaring JavaScript Files
All your JavaScript files need to be declared in config/importmap.rb so Rails
knows to include them in the import map object.
# config/importmap.rb
# Declare JavaScript files from your application
pin "application" # app/javascript/application.js
pin "utilities" # app/javascript/utilities.js
# Declare all files inside a folder
pin_all_from "app/javascript/controllers", under: "controllers"
All files declared in your config/importmap.rb must exist within your
asset pipeline's load paths.
This will create an import map object similar to:
<script type="importmap" data-turbo-track="reload">
{
"imports": {
"application": "/assets/application-d8a8613a.js",
"utilities": "/assets/utilities-e8dc057d.js",
"controllers/application": "/assets/controllers/application-3affb389.js",
"controllers/hello_controller": "/assets/controllers/hello_controller-708796bd.js",
"controllers": "/assets/controllers/index-ee64e1f1.js"
}
}
</script>
which is rendered in your HTML document's <head> using:
<%= javascript_importmap_tags %>
It's worth reiterating that the import map only defines the mapping between a module specifier and a file. It doesn't execute or import any code. As part of the above declaration, Rails also renders:
<script type="module">import "application"</script>
This is the default entry-point for your JavaScript application
(located at app/javascript/application.js). Use this file to import
additional JavaScript files such as your Stimulus controllers or
the utilities file shown in the above examples.
You'll notice that the filenames contain a hash. This is added by Rails' Asset Pipeline. It is calculated based on the file's contents and used to version the files.
See the Asset Pipeline guide and
the importmap-rails Readme for
further information.
3. Using a JavaScript Bundler
You can integrate a JavaScript bundler into Rails using the
jsbundling-rails gem. It supports
a number of bundlers such as ESBuild,
Rollup, Bun, and
Webpack.
jsbundling-rails requires a JavaScript runtime. For all bundlers
except Bun, you'll need Node.js. For Bun, you'll just
need to install that as it is both a JavaScript runtime and a bundler.
jsbundling-rails will automatically detect the JavaScript bundler, so
you may use alternatives such as Yarn without additional configuration.
3.1. Installing Node.js and Yarn
Find the installation instructions on the Node.js website and verify it’s installed correctly:
$ node --version
v23.6.1
To install Yarn, follow the installation instructions at the Yarn website. Verify it's installed using:
$ yarn --version
1.22.19
3.2. Installing Bun
Follow the installation instructions at the Bun website and verify it’s installed:
$ bun --version
v1.3.13
3.3. Installing jsbundling-rails
When creating a new Rails app, setup a JavaScript bundler using the -j or
--javascript flag:
$ rails new my_new_app -j esbuild
$ rails new my_new_app --javascript=esbuild
Add the jsbundling-rails gem in an existing Rails app using:
$ bundle add jsbundling-rails
Then configure your chosen bundler with:
$ bin/rails javascript:install:[bun|esbuild|rollup|webpack]
When using jsbundling-rails, use bin/dev to start the JavaScript bundler
along with Rails server in development. Further information is available in the
Asset Pipeline guide.
4. Delivering Bespoke JavaScript Files
You may wish to use Rails to deliver a pre-built or bespoke JavaScript file. This is useful if your JavaScript application doesn't use ESM, or requires a bundler such as Babel which isn't natively supported within Rails.
You can reference any JavaScript files in the
asset pipeline's load path using javascript_include_tag:
<%= javascript_include_tag "application" %>
which will render
<script src="/assets/application-5bcb24fe.js"></script>
A common setup in this case would be to write your built JavaScript files to app/assets/builds
(which is in the asset pipeline's load path), and load them into your HTML document
using javascript_include_tag.
Configuring your chosen bundler within Rails is out of scope for this guide. All supported bundlers are designed to work with ESM and are covered in the previous section: Using a JavaScript Bundler.
Files within app/assets/builds are excluded from source control by default.
5. Adding npm Packages
5.1. Vendoring npm Packages with importmap-rails
When using importmap-rails, npm packages are downloaded into the vendor
folder in your app and checked into source control.
Add a package to your application using bin/importmap pin:
$ bin/importmap pin ahoy.js
This will download the package into your vendor folder and declare it in
your config/importmap.rb. You can then import the package wherever required:
// app/javascript/application.js
import ahoy from "ahoy.js";
Or in a Stimulus controller:
import { Controller } from "@hotwired/stimulus"
import ahoy from "ahoy.js";
// Connects to data-controller="ahoy"
export default class extends Controller {
connect() {
ahoy.trackView()
}
}
Further information is available in the
importmap-rails Readme.
5.2. Installing npm Packages with a JavaScript Bundler
When using Bun, the Bun package manager installs npm packages:
$ bun add ahoy.js
See the Bun documentation for more information.
For all other bundlers, use Yarn to manage your dependencies:
$ yarn add ahoy.js
Further details are available in the Yarn documentation.
6. Choosing Between an Import Map and a JavaScript Bundler
In all new Rails apps, JavaScript is delivered using an import map. The Rails team believes that using an import map reduces complexity, improves developer experience, and delivers performance gains.
For many applications, especially those that rely primarily on Hotwire, an import map will be the right option for the long term. You can read more about the reasoning behind making import maps the default in Rails 7 here.
However, there may be use cases that call for a JavaScript bundler. Listed below are a few considerations where a JavaScript bundler may be more suited to your app than an import map:
- You cannot serve your assets over HTTP/2.
- Your code requires a transpilation step, such as JSX or TypeScript.
- You need to use JavaScript libraries that include CSS or otherwise rely on Webpack loaders.
- Your JavaScript architecture requires tree-shaking.
- You're using the
cssbundling-railsgem to manage your CSS.
7. Rails' JavaScript Libraries
Rails's default front-end stack is Hotwire which is
installed by default in all new Rails apps. A utility library called
request.js can be optionally
installed to simplify HTTP requests using JavaScript.
7.1. Hotwire
Hotwire consists of the libraries Turbo and Stimulus
which integrate with Rails via the gems turbo-rails and stimulus-rails.
Refer to the Using Hotwire in Rails guide for details.
7.2. request.js
Rails protects against
CSRF attacks by
validating non-GET requests with a token.
The request.js library automatically
adds the CSRF token to HTTP requests, making it easier to trigger HTTP requests
using JavaScript.
This library is maintained by the Rails team but it isn't included in Rails by default, so you'll need to install it:
$ bundle add requestjs-rails
$ bin/rails requestjs:install
Here's an example of a Stimulus controller that uses request.js to make a
POST request:
import { post } from "@rails/request.js"
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "input" ]
async fetchSuggestions() {
const response = await post("/users/suggestions", {
body: JSON.stringify({
input: this.inputTarget.value
})
})
if (response.ok) {
// Do something with the response
}
}
}
request.js will automatically activate JavaScript responses which have a
content-type response header of application/javascript or
application/ecmascript. It will also automatically execute Turbo Stream
responses.
See the Readme for advanced usage and further installation information.
Prior to Rails 7, a JavaScript library called Rails UJS was used to enhance Rails on the front-end. This library has now been removed from Rails, and all its functionality has been replaced by Turbo, Stimulus, and request.js. You can find information about Rails UJS in an older version of the guides.