Moving from Rails to Vue Saved Me Headaches — and $84/Year

Image from Tiago Gerken on Unsplash. Logo by Evan You

Grab a bowl of buttery popcorn because the saga continues! The end of the previous chapter saw our intrepid little Rails app camping happily on Heroku. This latest chapter in the hosting chronicles brings with it a twist: not just a change in host, but a complete migration from one programming language to another.

The app is no longer on Heroku, and it’s no longer written in Ruby or Rails. It’s written in JavaScript, hosted by Netlify, and it’s getting by with a little help from its new friend, Vue.js.

Onward for the Why’s, How’s, and Gottchas of the switch.

Table of Contents

Going off the Rails

I know, I know — Comparing Rails to Vue is comparing watermelons to grape skins. Rails is the whole stack, the noblest of the majestic monoliths — Vue is just the, well, “View” layer, the front end.

That’s just it: I didn’t need the whole stack on this project, at least not in the end.

Background: RunbyPace is the hobby app discussed in this article. It provides runners with pace prescriptions. The source code is on GitHub .

In the beginning, I intended to enable logins and user preferences and even training plans, etc. Rails is battle-tested and perfect for such a use case. So I put the MVC web stuff in Rails , and added a dash of “separation of concerns” by encapsulating the real logic in a well-tested Ruby gem .

As explained in the preceding article, I hosted my app on DigitalOcean, then AWS, and finally Heroku, after the AWS free-tier expired. I was happy with the outcome.

That’s when things kind of stagnated.

Pulling the weeds in my app patch

I was busy and wasn’t finding much tinker time for my project. Periodically I would get helpful alerts about a vulnerability in one of the Rails dependencies. That meant that time I intended to dedicate to my app went to vulnerability patching instead. Grumpy.

On top of that, the process of updating gems often wasn’t as easy as it could have been. Maybe it’s because Bundler is clunky and finicky, or maybe I just don’t know how to use it properly — perhaps both.

I was left pulling security weeds from a whole field when all I needed was a backyard garden

And so I’m asking myself: Why am I spending time patching dependencies instead of adding value to my app? There was no risk to user data because there was no user data. That said, I didn’t want the server compromised and used as a platform for all the dark things.

As things stood, Rails was too big for my needs. The price tag for its ample feature list was its broader attack surface. I was left pulling security weeds from a whole field when all I needed was a backyard garden.

Unused appendages

Then there was this database sitting there unused like a backpack of rocks my app had to carry around. I fully intended to make use of it, but never got around to it.

To be fair, it’s not Rails’ fault that I wasn’t using the database. Once I realized I wouldn’t need it, I could have configured Rails to run without a database. But Rails’ convention-oriented nature gave me pause — Any deviation from canonical convention can be like abandoning the trail for the blackberry thicket.

Antiquated front-end technologies

When I did get around to working on new features, I found the default state of front-end Rails development a little antiquated. It supported jQuery out of the box, but that was about it. Today the face of the Front End boils down to a choice between Angular, React, or Vue, and Rails didn’t integrate easily with these frameworks/libraries (although that appears to be changing).

I wanted to prototype new UI features quickly, but in Rails I felt like I was walking around with paint buckets on my feet. Even things like form validation felt overly difficult with Rails, at least for someone avoiding jQuery.

Philosophical discomfort

Then there were the philosophical considerations: I don’t really like MVC that much. It’s not a terrible way to organize your code; it definitely liberated us from the thorny tangled weeds of PHP’s early days by imposing some form of structure, but I’m not convinced it’s the best possible way to structure our front end code.

To me, the trend toward component-based architecture is much more compelling, and feels like a better application of the principle of Separation of Concerns. MVC and MVVM definitely separate concerns — I’m just not convinced they separate the right ones.

(I also think convention over configuration can be taken too far, but that’s a discussion for another time.)

A gem-encrusted prison of my own making

My Ruby gem also left me feeling trapped. Ruby has but one real web framework to champion it’s cause: Rails. I’d made a sparkly Ruby gem that I was proud of, but it lacked mobility. JavaScript doesn’t interface with Ruby code. That would require the Ruby runtime, and until web browsers start rolling with a Ruby engine, Ruby will only ever run server-side.

$7 and a summary of grievances

The arrival each month of the $7 Heroku bill was a reminder that my app was stagnant, strapped to the rails, and the light at the end of the tunnel was actually a series of trains — GitHub and Hakiri kindly notifying me of the latest security vulnerability discovered in my app’s dependency graph.

Rails is an excellent framework. However, I believe in choosing the right tool for the job. At least where this app is concerned, the issues I faced with Rails are listed below. (More * mean more frustration.)

  • Endless vulnerability patching****
  • Lack of built-in support for newer JavaScript approaches***
  • Lack of mobility for Ruby code**
  • Supporting unused features*
  • Tired of MVC*
  • $7/month pinhole in my wallet*

This time, our valiant little app was restless, not just for a new home, but for an identity, a whole new personality.


Vue, I choose you

I’d invested a lot of time in polishing and buffing my beloved Ruby gem, but ultimately that effort was doomed, because the code wasn’t portable. My thoughts turned to JavaScript, which basically runs everywhere, and that trend will likely continue. It’s similar to Ruby in its flexibility and lack of type-safety, so I expected that a port from Ruby to JS would be pretty painless. (Spoiler: It was.)

If I ever got around to wiring up a back end, the options are near limitless. Propping up another Rails API would be easy enough. However, I’d probably keep the whole stack on JavaScript by choosing Node.js — It’s fast enough, and the client and server could share some model and validation code. Furthermore, I’d likely forgo managing my own back-end service and take the whole thing serverless by building on top of a managed platform like Firebase.

So that’s why I chose JavaScript, but why Vue?

There are three major players in the JavaScript ecosystem: Angular, React, and Vue. All three of them are very active, and thus well-supported.

  • Angular: Sponsored by Google. A very heavy framework better-suited to large organizations. Similar to Rails, it sports lots of “magic”, which is makes it time-consuming to learn. Once you know it, the magic is a boon — until something goes wrong and you have to peel off layer upon layer of leaky abstractions to get at the bug.

  • React: Sponsored by FaceBook. A heavy JavaScript library. Uses components.

  • Vue: An extremely lightweight JavaScript library, which also uses component-based architecture. Limited ceremony enables fast prototyping.

Since I wanted my app to be as lightweight as possible, and I wanted to develop as quickly as possible whenever I had some spare time, I chose Vue.js.

Migration consternation

Never having used Vue before, I went for the Minimum Viable Product approach, setting aside my preference for TDD and automated testing until I had a working prototype in place.

Step 1: Reproduce the UI

The first step was to reproduce the visual design of the app. Essentially, this was as easy as copying and pasting the contents of the Rails view templates into the Vue templates, then replacing the Rails template <%= ... %> syntax with Vue’s {{ "Mustache" }} syntax.

For example,

<div class="text-muted"><%= run_type.description %></div>

might become:

<div class="text-muted">{{ this.runType.description }}</div>

Replacing a Rails helper like select_tag involved more work. For example, here was the form group for the run type selector in Rails. The select_tag helper is nice and concise.

<div class="form-group">
  <%= label_tag(:run_type, 'Today, I plan on running:') %>
  <div class="input-group">
    <%= select_tag(
      :run_type,
      options_for_select(TargetPaceHelper::all_run_types, :DistanceRun),
      {:class => 'form-control'})
    %>
  </div>
  <div class="text-muted" id="run_type_explanation">
    What exciting adventure will your run lead you on today?
  </div>
</div>

Now let’s see this same form group, post Vue migration. The select_tag equivalent is highlighted. The most interesting part is the v-for, which generates the list of options from a collection on the fly.

<div class="form-group">
  <label for="run_type">Today, I plan on running:</label>
  <div class="input-group">
    <select name="run_type"
      @change="changeRunType()"
      class="form-control"
      v-model="runTypeOption">
      <option v-for="run in lib.runTypes"
              v-bind:value="run.code"
              v-bind:key="run.Code">
        {{ run.name }}
      </option>
    </select>
  </div>
  <div class="text-muted">{{ this.runType.description }}</div>
</div>

Step 2: Update Bootstrap v3 to v4

Then there was the matter of Bootstrap… Bootstrap relies on jQuery, but we don’t need another JavaScript library when we’ve got Vue. With Vue in the picture, even two is a crowd. Fortunately, there’s Bootstrap-Vue . Problem solved.

Unfortunately(ish), Bootstrap-Vue uses the Bootstrap v4 syntax, while my aging app was stuck on Bootstrap v3. That caused some minor layout and behavior issues, the worst being that the hamburger button on the collapsed navbar was broken. Since both Bootstrap and Bootstrap-Vue are well-documented and this is a tiny app, it was easy to update the style classes.

Remember that busted navbar? Because it relies on jQuery in Bootstrap, Bootstrap-Vue replaces it with a whole new navbar : <b-navbar>. In the code samples below, notice that the Bootstrap-Vue syntax is cleaner and less verbose.

The old-school Bootstrap V3 navbar:

<nav class="navbar navbar-default navbar-fixed-bottom">
  <div class="container-fluid">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
              data-target="#navbar-collapse-1" aria-expanded="false">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="/">&#x1F3C3; Runby Pace</a>
    </div>
    <div class="collapse navbar-collapse" id="navbar-collapse-1">
      <ul class="nav navbar-nav">
        <li role="presentation" <%= class_list_if(@active_nav == 'index', 'active') %>">
          <a href="/">Home</a>
        </li>
        <li role="presentation" <%= class_list_if(@active_nav == 'about', 'active') %>>
          <%= link_to('About','/about') %>
        </li>
      </ul>
    </div>
  </div>
</nav>

…became:

<b-navbar fixed="bottom" toggleable="md" type="light" variant="light">
  <b-navbar-toggle target="nav_collapse"></b-navbar-toggle>
  <b-navbar-brand to="/">&#x1F3C3; Runby Pace</b-navbar-brand>
  <b-collapse is-nav id="nav_collapse">
    <b-navbar-nav>
      <b-nav-item to="/">Home</b-nav-item>
      <b-nav-item to="/about">About</b-nav-item>
    </b-navbar-nav>
  </b-collapse>
</b-navbar>

Step 3: Form validation

The Rails app never had much front-end form validation, largely because of my unfounded aversion to jQuery. So when I introduced some basic form validation to my Vue app, I was really just implementing a long-overdue feature.

That said, form validation in Vue is super easy, even if you don’t use a validation library. To keep things simple, I just added computed properties like invalidRaceTime(), then bound the validation message to another computed property called raceTimeValidationMessage(). Bootstrap took care of the styling, courtesy of the is-invalid class.

Here’s a sample of this hand-rolled validation in RunbyPace.vue .

<template>
  ...
  <input type="text"
         v-model="fiveKmRaceTime"
         v-bind:class="{
          'form-control': true,
          'is-invalid': this.invalidRaceTime }"
         placeholder="A recent 5K race time, like 21:30" />
  <div class="invalid-feedback">{{ this.raceTimeValidationMessage }}</div>
  ...
</template>
...
<script>
export default {
  name: 'RunbyPace',
  ...
  computed: {
    missingRaceTime() {
      return this.submitted && this.fiveKmRaceTime === '';
    },
    invalidRaceTime() {
      return this.missingRaceTime ||
       (this.submitted && !this.lib.validTime(this.fiveKmRaceTime));
    },
    raceTimeValidationMessage() {
      if (this.missingRaceTime) {
        return 'Don\'t forget your 5K race time';
      }
      if (this.invalidRaceTime) {
        return `'${this.fiveKmRaceTime}' is not a valid race time. Try 21:44.`;
      }
      return '5K Valid';
    },
  }
  ...
</script>

This homespun validation could get unwieldy on larger forms, so I plan on replacing it with a validation library like Vuelidate .

Step 4: Migrate domain logic

Next up was migrating the core domain logic, that of calculating a runner’s target paces. The Ruby gem encapsulating this logic is object-oriented and fairly feature rich — In fact it does a lot more than its humble pace calculating frontman. For this initial port from Rails to Vue, I went for a “needs driven” approach: If you don’t need it right now, don’t code it. The result? The fairly extensive Ruby gem was replaced with a JavaScript code library weighing in at around 200 lines.

In the Rails app, all the number crunching took place server-side. Naturally, this was reversed in the Vue app, and all the calculations happen on the client. There is no app server, just plain old web servers serving static files from a CDN. This could result in tremendous cost savings should the app scale, because the computational heavy-lifting is delegated to the client device it’s running on. This works because the calculations aren’t too intense, and I’m not worried about revealing trade secret algorithms.

One thing that’s sorely lacking? Tests! I manually tested the code as I went, comparing results produced by the two apps until I was satisfied I’d achieved behavioral parity. I took a break from TDD for this exercise because of the exploratory nature of the migration project. Now that I’ve got a working prototype in place, the purist in me demands a TDD rewrite. The pragmatist is okay with a test-last approach.

Step 5: Add Pages

Out of the box, Vue builds Single Page Apps (SPA). If you have multiple pages, you’ve got to add routing. The Rails app sported an About page, and the runner’s prescribed pace is also displayed on its own page. (I’m not happy with displaying the target pace on its own page, but as UX McCoy might’ve said, “It’s a migration, not a UX design session, Jim.”)

The old Rails router is below. I do love it’s clear brevity.

Rails.application.routes.draw do

  namespace :static_pages, path: '/' do
    get 'about'
    get 'health_check'
  end

  get 'target_pace/calc'

  root 'target_pace#index'

While not as terse, Vue also makes it easy to add “pages” by adding a router. It’s still a Single Page App (SPA), but Vue takes care of updating the DOM so that it looks like a new page.

After installing the vue-router npm package, I added src/router/index.js, which looks like this:

import Vue from 'vue';
import Router from 'vue-router';

import RunbyPace from '@/components/RunbyPace.vue';
import TargetPace from '@/components/TargetPace.vue';
import About from '@/components/About.vue';

Vue.use(Router);

export default new Router({
  mode: 'history',
  routes: [{
    path: '/',
    name: 'Home',
    component: RunbyPace,
  }, {
    path: '/targetPace/:fiveKmRaceTime&:runTypeCode',
    name: 'targetPace',
    component: TargetPace,
    props: true,
  }, {
    path: '/about',
    name: 'About',
    component: About,
  }],
});

Each route has its path, name, and the component that will be navigated to. The <code>props: true</code> option is important for decoupling components from their route. When set, the component’s route.params will be automatically set as the component props.

But how do you wire up this router with the rest of the app? The important parts from src/main.js are:

// src/main.js
import router from './router';
...
new Vue({
  render: h => h(App),
  router,
  store,
}).$mount('#app');

All we’re doing there is importing the router created above, then binding it to the app.

Finally, in src/App.vue, we must decide where our view will be rendered on the page using the <router-view> tag:

<template>
  <div id="app" class="container">
    ...
    <router-view>
    ...
  </div>
</template>
...

Step 6: DRY it out

At this point, the app was basically migrated. There was just one problem: the code wasn’t “DRY”.

I had duplicated domain logic across two different components. While I cringed slightly as I copied and pasted the code from one component to the other, I just wanted to get things working. I’d worry about refactoring later.

The first big refactor was to extract the duplicate logic from the components into the runbylib.js library mentioned earlier. Now I just needed a way to share the library between the components.

To share code, state, and static data between the Vue components, I opted for Vuex . It “serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion.”

After installing Vuex with npm install vuex --save, I initialized it in main.js, like so:

import Vuex from 'vuex';
...
// The library I want to share amongst the components
import RunbyLib from './runbylib';
...
Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    RunbyLib,
  },
  mutations: {
  },
});

Then I just had to make sure that all child components would receive a reference to this singleton store object. That was as easy a providing it to the new Vue object.

new Vue({
  render: h => h(App),
  router,
  // Provide the store using the "store" option.
  //  this will inject the store instance to all child components.
  store,
}).$mount('#app');

Now each component would have access to the RunbyLib library through this.$store.state.RunbyLib. That seemed like a lot to type, so I aliased that to this.lib in each component, using beforeCreate(). Here’s an example from the RunbyPace.vue component:

export default {
  name: 'RunbyPace',
  beforeCreate() {
    this.lib = this.$store.state.RunbyLib;
  },
  ...

That’s it! The app wasn’t perfect, but I felt it was shippable. I deployed it to Netlify and Bob was my uncle! Bob and I exchanged a series of increasingly awkward high fives.

If your nose is recoiling from the acrid whiff of revisionist history, I perceive that this, sir or madam, is not your first rodeo. — Things are never quite so easy.


Unfiltered portraits of a face-planting “Vuebie”

This migration project was literally my first foray into Vue.js. Guess that makes me a “Vuebie”? :) Here’s an unfiltered look into my goofy free-wheeling inverted Vue face plants.

Snowboarder wiping out
Photo by Mattias Olsson on Unsplash

() => { return “Oops” }

My first big mistake was trying to use ES6 arrow functions in a calculated field. Intuitively it made sense, so I was truly dumbfounded when the console displayed nothing but “this is undefined.”

I chased red herrings until they turned into cows and came home. I poked, peeked, and prodded under every stone of cold unfeeling code. When I thought I’d exhausted every option and hung clinging to the bridge by two white-knuckled fingers, a 200 foot drop into bleak surrender below me, Stack Overflow came to my rescue like Superman wrapping a steely arm around my waist and lifting me to safety.

Turns out I was just another victim to the nuances of this. From the Docs: “Since arrow functions are bound to the parent context, this will not be the Vue instance as you’d expect.”

If you want to see a code snippet, I’d written something like this:

computed: {
  // Don't do this
  missingRaceTime: () => {
    return this.submitted && this.fiveKmRaceTime === '';
  },
  ...

But it should have looked like this:

computed: {
  // Do this
  missingRaceTime() {
    return this.submitted && this.fiveKmRaceTime === '';
  },
  ...

The nice thing about intractable bugs like this is that you end up reading your code over and over, scrutinizing every line, every curly brace, every comma, under the hazy microscope of your newly acquired knowledge. It’s when things go wrong that you’re forced into a deeper understanding about the language or library. Smooth-sailing through a detailed tutorial often leaves you with a superficial understanding at best — and more often cluelessness.MAX.

Danger: Daft dude deploying duffs

“Look Ma! Multiple A records!”

Deploying a Vue app on Netlify is almost too easy. It literally takes about two minutes . At least it should for most people.

Whenever you’re preparing to switch hosts and fiddle with DNS, it’s good practice to lower the TTL timeouts on your DNS records to five minutes or so. That way, if you make an invalid entry or something goes wrong, you don’t have to wait 24 hours for DNS caches to clear across the internet. Naturally, I didn’t do this.

I switched DNS providers from PointDNS to Netlify, but without lowering the TTLs on my DNS records. For the next 24 hours, my site had two different IP addresses — it just depended on who you asked, and how much time was left on their DNS cache. “Look Ma! Multiple A records!” Now if only that were the only problem.

…it came to life like a killer robot that eats names, spits out numbers, and charges you 50 cents.

Netlify automatically provisions free SSL certificates for sites on its platform, thanks to the amazing Let&rsquo;s Encrypt Certificate Authority. My site got the www.runbypace.com SSL certificate okay, but the certificate configuration for the bare runbypace.com errored out with “Domain runbypace.com has multiple A records”. Oops. That meant that anyone who went to the naked runbypace.com domain would get a scary “this site is not secure” warning.

I used DNSViz to try to get more insight into what was happening. I got insight alright. Remember how RunbyPace used to be hosted on AWS? Well it turns out I still had a hosted zone on Route 53! It was lying dormant until I tried moving to Netlify. Then it came to life like a killer robot that eats names, spits out numbers, and charges you 50 cents.

I tore down the Route 53 stuff and waited an entire day, but Netlify still complained about “multiple A records” and wouldn’t provision the SSL certificate.

It was time for more troubleshooting:

  • I tried flushing Netlify’s DNS cache using Google&rsquo;s Flush Cache tool , but that didn’t help.
  • I payed DNSViz another visit. It showed three A records for runbypace.com, all pointing to IP addresses registered to DigitalOcean. At first I thought I was on to something, but then I realized I was just looking at the load-balanced backend of Netlify.
  • After waiting a few days, I decided that all the DNS caches must have expired, so something was messed up with my Netlify setup. I deleted the app in Netlify and recreated it. Fortunately, being Netlify, that takes less than 5 minutes… And this time, everything worked flawlessly.🎉

This “face plant” underscores one of the benefits of hobby projects: You can mess around and fail hard with relative impunity.

404 on cold routes

“Yeah I got nothin’. ¯\_(ツ)_/¯ Here’s a nice 404 for your troubles.”

By now I was doing a wee little jig of joy, pleased that everything was working, and by how quickly this first-pass migration went. But not quite everything was working.

If I was on a “page” other than the home page and hit Refresh, Netlify served me a 404 Not Found. For example, I tried to navigate straight to https://www.runbypace.com/targetPace/21:30&amp;DistanceRun instead of plain old https://www.runbypace.com . The problem didn’t happen while testing locally using the Vue CLI. In hindsight the solution seems obvious, but at first I was stumped.

404 Not Found from Netlify
Have you seen my mummy?

The problem was that the Vue router was never being hit. The web server saw a route like /targetPace/ and said, “Yeah I’ve got nothing. ¯\_(ツ)_/¯ Here’s a nice 404 for your troubles.”

With Netlify, the solution was super simple: Add a one line ./public/_redirects file that redirected everything to index.html.

# _redirects
/*  /index.html  200

Now all traffic is redirected to index.html (with a friendly “200 Success” code) and, most importantly, the route is picked up and processed by our Vue router.


What’s next

This initial port from Rails to Vue is quite bare-bones, or “MVP”, to borrow an acronym from Eric Ries’ Lean Startup(affiliate link) . In this context, MVP stands for “minimum viable product”, not “most valuable player.” ;-)

There is still much to do.

  • The previous stack had much better test coverage, particularly surrounding the ruby gem. All of that got dumped. Build a good test suite to prevent regressions as I add features.
  • After the tests are in place, run them as part of the CI build.
  • The JavaScript library containing the domain logic will bloat as I add features. Consider alternatives, perhaps extracting it into its own npm package.
  • Refine and enhance the UX. I’m hopeful that Vue’s component approach will be a much better platform for this.

As you can tell, I’m no expert in either Rails or Vue, but I’m enjoying the journey. If you notice mistakes or know of a better way, please correct me. Thanks, and happy coding!

Avatar
Ty Walls
Digital Construction Worker

Ty Walls is a software engineer in love with creating, learning, and teaching.

Related