Building a JavaScript Single-Page Application with Rails API

Railway lines stretching into a foggy urban landscape.

This is an overview of how to build a JavaScript single-page application with a Rails API on the backend utilizing a monorepo. In this example, I focus on a small application I built called SeeData Visualizer.

The build of this application can be broadly split into two sections: the Rails API and the JavaScript client.

The Rails API

To create a monorepo, start with a single folder for the entire project. Following convention to name folders and files based on their content, I called the housing folder for the example application ‘seedata-visualizer.’

Within the folder, create a new Rails API from the terminal using the following command:

terminal $ rails new project_name --api

Once Rails finishes creating the API, make a new folder within the project called ‘projectname_client’ for all of the JavaScript, CSS, and HTML used by the frontend. Since I used a monorepo, I tried to keep naming very clear; I called the API side ‘seedata_api,’ so I called the client side ‘seedata_client.’ At the highest level within the project folder, all that should be visible are these two folders and the readme file.

Screenshot of application file tree at the highest level, showing the API folder, client folder, and readme.

Connecting to GitHub & the First Commit

Before any other changes are made, the project’s structure should be initialized as a GitHub repository and linked to a remote. I prefer to do this by initializing a repository with the same name as my project on GitHub and then setting it as the remote origin from my project.

Initialize a new repository on GitHub. Use the project name, with no spaces, as the repository name. I avoid checking any of the boxes in the ‘Initialize this repository with:’ section because the readme makes a good first commit and I prefer to add the license later. This is a matter of personal preference.

An screenshot of the new repository form on GitHub showing where to put the project name and my preferred configuration of initialization conditions (public, no extras added)

Once the repository has been created, copy its link. In a terminal, access the API folder of the new project. Rails automatically initializes a GitHub repository when it creates an API, and this will cause problems with committing the entire project if it is not removed. Once in the API folder, execute the following command in order to remove the GitHub repository Rails automatically initialized:

terminal $ rm -rf .git

Once the auto-generated git files have been removed, go back to the parent project directory in terminal and initialize a GitHub repository :

terminal $ cd ..
terminal $ git init

Optionally, a branch name can be added when the repository is initialized locally. GitHub now natively calls the main branch ‘main,’ but it used to use ‘master,’ so some coders prefer to call their main branch master. To initialize the local repository with a branch name, use the following:

terminal $ git init -b branch_name

Next is a crucial step: make sure something in the repository has been edited. I usually use the readme for this purpose. In the main directory, add a file called README.md and type some text into the file. Next, stage the first commit and add the remote by pasting the GitHub repository link:

terminal $ git add .
terminal $ git commit -m 'initial commit'
terminal $ git remote add origin <your github repo link>

The last thing to do is push the commit to the remote:

terminal $ git push

If the first push is successful, the entire project can now be committed from the topmost directory. If commits are attempted from one of the subdirectories, GitHub will render error messages and the commits will fail. During the coding of the example application, I kept a terminal window open to the main folder to make commits.

Completing the Backend

The API has been set up and the repository is linked to GitHub. Any relationships between the data in the API should now be added before the frontend is built. In the example project, a Visualization belongs to a Dataset, and a Dataset has many Visualizations.

Run migrations and add any seed data to the database:

terminal $ rails db:migrate
terminal $ rails db:seed

The last step to completing the backend is to enable CORS, cross-origin resource sharing. This enables fetch requests to be made from outside of the API server, which is necessary to properly run the JavaScript frontend. First, enable the gem ‘rack-cors’ by uncommenting it in the Gemfile (Rails automatically includes it but comments it out).

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possiblegem 'rack-cors'

Last, in the application.rb file within the config folder, paste the following (or uncomment, if already included) into the ‘class Application < Rails::Application’ section of the file:

config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*',
:headers => :any,
:methods => [:get, :post, :delete, :put, :patch, :options, :head],
:max_age => 0
end
end

Nothing should be removed; this code should simply be added into the pre-existing class. This version of the code allows calls from any origin for any resource, so it is not a long-term solution for a hosted application. However, for a test project or practice, this configuration is fine.

Writing the Client: File Structure

The frontend of the application has a much smaller file tree than the backend. The bare minimum necessary files are index.html and index.js, but if using object-oriented JavaScript, it is recommended to create a source folder containing separate JavaScript files for each class and its AJAX calls.

Example of the frontend file tree showing the source folder, index files, and CSS folder.

In order to include these files in the rendered application, they must be called in index.html. Script files (JavaScript in this case) can be called in the headers of the index file and then run with an event listener for DOM loading, or they can be called at the end of the index file, which will ensure that the DOM content loads before the script files are run. I prefer the latter approach.

The bottom of the example application <body> tag, showing the scripts called for index.html.

Because index.js contains the global context of the JavaScript in the application, it is loaded last. For simpler applications, listing it last will maintain this order. The example application involves fetch race conditions which are solved externally, so in this case the defer attribute is set to hold index.js until last.

For any styling with vanilla CSS, the stylesheet can be called in the headers using a relative link. Some CSS libraries do not employ this technique, so the headers may or may not be used to call styling depending on exactly how CSS is implemented.

Screenshot of application headers showing relative link to stylesheet.

Client-Server Communication

The point of setting up an application with an API backend and a JavaScript frontend is to make use of the fetch AJAX utility to communicate between the server and client sides. These communications should be housed within files like ‘visualizationService.js,’ named for their class in object-oriented JavaScript. This is distinct from the main class file ‘visualization.js,’ which holds behaviors associated with processing and placing elements on the DOM.

A basic fetch request involves three parts: information, configuration, and the request itself. In the following example, the constant visualization is an object containing the information to be sent to the server, the configObj is the configuration, and the request itself is the fetch.

createVisualization(){
const visualization = {
name: document.getElementById('pass_name').value,
chart_type: document.getElementById('pass_type').value,
x_choice: parseInt(document.getElementById('x-data').value),
y_choice: parseInt(document.getElementById('y-data').value),
dataset_id: parseInt(document.getElementById('pass_dataset_id').value)
}
const configObj = {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(visualization)
}
fetch(`${this.endpoint}/visualizations`, configObj)
.then(resp => resp.json())
.then(visualization => {
const v = new Visualization(visualization)
v.addToDom()
})
}

The very last step is to add elements to the DOM based on data in the server that has been communicated to the frontend using fetch requests. This typically occurs in parts, but the simplest method is illustrated in the previous example: for each piece of data returned from the server, initialize a new object of the correct class and then add it to the DOM. Within the visualization class, the methods being called are the constructor and addToDom():

The actual constructor function for visualizations in the application.
addToDom(){
const renderTarget = this.element
this.renderVisualization(this.name, 0.35, renderTarget)
}

The addToDom() function above is a simplified version of the actual function in order to show the bare essentials of the process, while the screenshot of the constructor is the actual constructor function.

Go Forth

With a basic API setup, server-client communication, and a working frontend that can post data to the DOM, the options are endless. In my case, I used a JavaScript library called d3.js to render data visualizations, and figured out how to accept user-uploaded CSV files to create datasets. The site I wrote features the ability to add datasets, create visualizations from new or existing data, and view and delete visualizations.

Using a Rails backend provides a reliable and flexible data storage solution, while JavaScript on the frontend creates a fast and user-friendly experience. The combination is ideal for quickly building functional applications.

A YouTube walkthrough of the SeeData Visualizer application

Former archaeologist and high school teacher turned software engineer. Just trying to learn and solve puzzles!