Setup Single Page Application in Rails with Vue 3 and Typescript (Vue CLI)

Photo by Liudmyla Denysiuk on Unsplash

Note: Go to Setup Rails section below to skip my story. Just in case you are in hurry and want to see the guide immediately.

Just want to give you a heads up. Everytime I mention Rails in this article, it refers to Ruby on Rails. Now let’s start the story.

Me and my team have recently been tasked to write a new service. Since we have experience building and maintaining current services in Rails 5.2, we decided to use Rails again for this new service.

But we want to take it to the next level to spice things up a little bit. This means we use the latest version of everything that is needed to build this new service. Instead of using Rails 5.2, we use the latest version of Rails, which is Rails 6. This also applied to the Ruby version. We are using Ruby 2.7 as the base image of the application.

Oops, sorry. I forgot to mention the new service that we are trying to build will be running on Docker. However, this guide is also applicable in any environment you have right now, either using rvm, or direct ruby environment on your local machine. So don’t worry. Everything will be fine. (I hope).

Back to the story. We also want to build this new service into a single page application, which means we will be using Vue.js as our frontend framework. Why Vue.js? Well, because it’s awesome. But seriously, no arguing needed here. Any javascript framework is also great and it’s your decision to use any of it. We just decided to use Vue.js because the framework is first-class citizen of the frontend framework in Mekari.

Defining backend framework, check. Defining frontend framework, check. Now how do we make them work together? This is an interesting question. Rails itself has built a gem that helps us to write a single page application using our favorite frontend framework and webpack, it’s called webpacker.

Again, we want to use the latest version of everything, this means we want to use Vue 3 and Typescript which also means we need to use Vue CLI to build Vue 3 application with Typescript to make the development easier. While we found some articles that explain how to combine Webpacker and Vue CLI and make it work together. We are having problems to do it by ourselves that end up with a lot of errors that take time to solve.

So, instead of using Webpacker as the glue between Rails and Vue.js, why not just smash them together into one application that contains a Rails application as the backend and API framework and generated Vue.js application as the final result. And I think we have finally made it work.

Ok. Let’s end the story and start the guide.

Setup Rails


Setup Vue 3

$ yarn global add @vue/cli@next

Or if you want to use npm you can run:

$ npm install -g @vue/cli@next

Make sure it has been installed successfully by running:

$ vue --version

Then you can go to the root directory of your Rails application and create new Vue.js project. This can be achieved by running:

$ vue create frontendVue CLI v4.5.6? Please pick a preset:
Default ([Vue 2] babel, eslint)
Default (Vue 3 Preview) ([Vue 3] babel, eslint)
> Manually select features
----(Press <space> to select, <a> to toggle all, <i> to invert selection)? Check the features needed for your project:
(*) Choose Vue version
(*) Babel
(*) TypeScript
( ) Progressive Web App (PWA) Support
(*) Router
(*) Vuex
(*) CSS Pre-processors
(*) Linter / Formatter
(*) Unit Testing
( ) E2E Testing
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)? Use class-style component syntax? No? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes? Use history mode for router? (Requires proper server setup for index fallback in production) No? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)? Pick a linter / formatter config: Prettier? Pick additional lint features: Lint on save? Pick a unit testing solution: Mocha? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files? Save this as a preset for future projects? (y/N) y

You don’t have to follow the options that I choose above. However since this is Vue 3 and Typescript setup, you need to make sure you choose 3.x (Preview) and Typescript. Once it done, it will create new frontend folder on your Rails directory.

Setup Integration

outputDir is telling vue-cli-service to generate output to public directory of Rails. This means either you remove all the files inside public directory together or you move your existing file on your /public directory to /frontend/public directory.

indexPath is telling vue-cli-service to generate final index.html output to to views folder of your application. Since I put general#index as root on /config/routes.rb, I put ../app/views/general/index.html.erb as indexPath configuration. However this can be any controller, depends on your needs, but you have to make sure that it is root url on routes.rb.

The final touch is you need to add public folder content and the indexPath view file on your git ignore. This is additional config on our .gitignore.

That’s it. You just need to start your Rails application and on your other console tab you open frontend directory and run yarn build --watch on it. So later everytime you make changes on your frontend folder it will be automatically rebuild by vue-cli-service and you can see the changes by reloading the site on the browser.

Rails Data Rendering

However, when it comes to POST request you might not be able to achieve this because CSRF restriction that Rails has. Usually in general Rails application we can use <%=csrf_meta_tags%> to generate html meta tag that can be used to store CSRF token for our ajax request. If we try to put that tag directly on /frontend/public/index.html file like this:

you will see an error that saying,

This is happening because there is no csrf_meta_tags defined on the build configuration. This can be mitigated by combining Vue.js html data rendering and Rails view data rendering, some sort of double templating mechanism. Here how it works.

In Vue.js application that generated by vue-cli-service, there is index.html file that generated on your frontend/public folder. Inside that index.html you can render any data you want using <%= %> tag. Vue has this data rendering capabilities because help of html-webpack-plugin module.

But <%= %> tag is limited to the Vue itself, meanings that once your Vue.js application rebuilt using yarn build. html-webpack-plugin module will try to render any data that available to the tag before rendering final html output to indexPath that we have set before. If the tags is not exist, it will throw an error.

Vue itself only allow environment variable data that prefixed with VUE_APP_* to be rendered with <%= %> tag. This means that you cannot pass other data to the tag, except via environment variable. So if we want to have a data rendered to index.html, you need to set it on environment variable, like this:


Since vue-cli-service is also using dotenv module to manage environment variable, we can simply put this data on .env file on frontend folder.

If we try to add VUE_APP_SOME_DATA to index.html using <%= %> tag like this:

It will generate this html output:

You see value hello_world there. After see how Vue.js and html-webpack-plugin works together to render data on html output, we can use this to insert any Rails variable we want that later will be rendered by Rails.

We can just achieve this by simply putting Rails view variables tag on the environment variable or .env file like this:

VUE_APP_CSRF_META_TAGS="<%= csrf_meta_tags %>"

and put the VUE_APP_* tags on the /frontend/public/index.html like this:

And once you build Vue.js app, it will generated html output on your indexPath like this:

Then later, when you refresh your site on the browser you will get this output:

As you can see, the html output now contain CSRF meta tag and it even able to render environment variable on Rails (<%= ENV['GOOGLE_ANALYTIC_ID'] %>)

Once we getting CSRF meta tag, you can attach the token on the content property on your ajax request. For example, if you are using axios to make request to the browser you can add this script before using make API call:


So, let me shared our *simplified* Dockerfile:

Just want to give you some information, we are using Puma as the web server of our Rails application. It might different with your setup which mean you will need to adjust some flow in your Dockerfile.


Lead Software Engineer at Mekari — Empower businesses and professionals to progress effortlessly (

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store