clock menu more-arrow no yes mobile
Train station at sunrise Photo by Charles Forerunner on Unsplash

Filed under:

Building a modern single-page app with Vue and Rails

What we learned by experimenting, working and learning together

This February, the Revenue Platforms team, comprised of folks who support our advertising projects like Hymnal and Concert, wrapped up work on a new internal app. We built it as a single-page app using the Vue JavaScript framework on top of Rails while leveraging a couple open-source tools to expose server data through a valid JSON-API specification.

Building a single-page app is a departure from many other tools at Vox Media, including our ad-creation platform Hymnal. Our team is familiar with traditional Rails views that include page loads between controller endpoints and a little JavaScript sprinkled on top to enhance functionality. This approach has always worked for us, but we wanted to become familiar with the modern landscape of JavaScript frameworks — and we did so by starting with a relatively small app.

Why Vue?

At the outset of this project, there was a lot of discussion about which front-end framework we’d be using. Why use something like Vue when React is clearly more popular?

Several factors pushed the needle toward Vue over React for our team. The first factor was Template syntax.

If you’re familiar with React, you’re probably familiar with JSX. For the uninitiated, JSX is often compared to writing XML-like syntax inside a JavaScript method.

This can be a powerful tool that provides direct access to JavaScript properties and computed methods. But it also presents a steeper learning curve than the Vue alternative: Templates.

Vue templates look like traditional HTML with additional attributes — called Vue directives — sprinkled in.

In addition to to the template syntax, Vue supports single-file components which bundle familiar HTML, CSS and JS tags together in the same .vue file. Our team found that it was a convenient way to structure our components and keep our project organized.

Vue was also a great choice for our team because several team members had experience with it in smaller projects. Plus, while it is a less-popular project than React, it still has a vibrant community and an active core development team.

Getting Started with Webpacker

Webpack has grown to be a popular choice for build tools recently, and we decided to compile our single-file Vue components, in additional to other modern JavaScript files, with Webpack.

And since we’re using Rails as our backend for this app, we chose to use the official Webpacker add-on for Rails. This proved to be a delight to use! It made setting up our initial Vue single-page app a breeze. With a single command, Webpacker scaffolded a simple Vue component and wired up its Webpack config under the hood to compile the components:

One upside to this approach is that you can skip writing your own Webpack configuration by hand. But this level of abstraction is a double-edged sword.

An example: shortly into the project, we decided it would be useful to be able to share a set of Sass variables between Vue single-file components when defining per-component styles. The Webpack add-on sass-resources-loader aims to make this simple. But since Webpacker was using vue-loader inside the abstracted Webpack config, our new Sass loader wasn’t being applied in the proper order required to compile single-file Vue components.

Additionally, vue-loader loads its own set of Webpacker loaders which can be customized – but using only a string format instead of the object format required by sass-resources-loader. A workaround exists, but it results in a configuration override like this:

One thing our team learned through this entire process is that these plugins are under active development. Both Webpacker and vue-loader released fairly significant updates during our development period. Using the “latest and greatest” often means sacrificing stability and maturity in software.

Working Together

Six engineers from our team worked closely together to build our internal app. This presented a few challenges, because none of us had really built a Vue single-page app before — and we had to coordinate who was going to do what.

To reduce confusion and to combat uncertainty, we focused on creating tiny, actionable tasks and assigning them to Github milestones. Each milestone represented a different chunk of functionality within the app. And as we progressed through each milestone, we discovered more tasks for that milestone.

Another strategy we applied was the separation of concerns between “front-end” and “back-end” work. While back-end engineers were focused on setting up the JSON-API in Rails, front-end engineers were building out Vue components. Since we couldn’t guarantee that all of the API calls would be ready to be consumed by the front-end by the time they were needed, our front-end engineers leveraged stubs — simple JavaScript objects and arrays — meant to mock the server-side responses.

As the back-end part of the app progressed, we gradually removed these stubs and replaced them with actual AJAX calls.

Some parts of collaboration can’t be planned in advance. For example: a few weeks into the project, we had a smattering of Vue files living in a single directory called components. This was fine because Vue isn’t opinionated about how you organize single-file components — but it started to feel a bit unwieldy.

Becca and Josh discussing file organization

These conversations happened organically through Slack over the course of several weeks. We eventually settled on a file structure that split pages and components into separate folders.

Pair Programming

Recently, our team has been making an effort to pair program more frequently. Since most of our engineers are remote, this involves one person “driving” and sharing their screen while one or more people watch and share their thoughts.

These pairing moments also happen organically through Slack:

Our experience pairing on this project reinforced our decision to try to pair more often. We found it less daunting to learn a new front-end framework like Vue when we were able to walk through writing code with our teammates.

Linting and Testing

When more than one engineer works together on a single project, it’s helpful to ensure the code being written is standardized using a linter. This is especially important in JavaScript, because there are many ways to accomplish the same task or format a single line of code. Each time someone has to stop and make a decision about whether to use an ES6 arrow function or whether to use a semicolon, it slows progress and unnecessarily uses brain power.

In addition to ESLint’s recommended settings, we added eslint-plugin-vue to our project so our Vue files could be linted, too. One rule in particular — order-in-components — was helpful because Vue instance definitions can become quite large, and searching for a specific property across several components was easier when each component was consistently-defined.

Another way we maintained code quality on a multi-person project was by writing tests. On the front-end, we wrote tests for Vue components using the official vue-test-utils library. While also a pretty new tool — it’s still in beta at the time of this post — it provided useful utilities to write tests for Vue components.

We found that a difficult part of front-end testing was stubbing the data our components normally fetched with AJAX. Our team experimented with the following three approaches to faking data for tests:

  1. Provide fake data for a component, and stub the initial fetcher method
  2. Stub the underlying JavaScript model object, and populate it with fake data
  3. Stub the HTTP requests built with native fetch() and used by the underlying JavaScript model library

The first approach was the simplest: stub the initial fetcher method, like fetchUsers() on the component, and set the fake data manually. While this worked, it limited our ability to write more complete integration tests: “How can we be sure that this component loads from start to finish, setting the given fake data, and renders the expected HTML?”.

We then attempted the second approach which was to fake the underlying JavaScript models provided to us by JSORM, a helper library meant to interface with JSON-API server requests and responses. This proved to be more verbose and felt a little hacky, because JSORM provides lots of helpers on each instance of a model which we would have to stub individually.

That’s when we landed on our third approach: stubbing the HTTP requests used by JSORM. This proved to be a little more difficult than using a fake XHR and server through Sinon, since JSORM uses the native Fetch API.

This allowed us to write more complete integration tests, even if the creation of stubbed endpoints and fixtures meant more work at the beginning.

Control Flow

One part of the Vue project we weren’t able to lint or test was the order of attributes in each tag. It might sound silly that this is something we’d need to consider, but take this example:

Notice the v-if and v-else tags. Since the first element has the directive immediately after the opening of the tag, it’s pretty obvious that it will render conditionally. But on the second element, v-else is not the first attribute.

When reading through a large component, our team noticed that this made it difficult to determine the control flow of certain dynamic parts of a given component.

Engineer Winston Hearn made a comparison to Ruby’s ERB files which require an explicit pair of <% %> tags for control flow. These tags tend to jump out at you more than a standard HTML tag with a directive attached.

Drew Amato, another engineer, mentioned that he’s never seen lines blur as much — between "markup" and "code" — as with a Vue project.

And engineer Becca Barton noted that it would be very helpful to quickly glance at any file and be able to see which pieces are just plain HTML and which are modified by Vue directives.

To solve this issue, we made a “handshake agreement” that we would always make the control flow Vue directives — v-if, v-else and v-show — the first attributes in the tag. This rule helped clean up our templates and made our code more readable:

Under the Hood: JSON-API Suite

Our team decided to adhere to the JSON-API spec when building out the server-side API for this project. Principal engineer Steve McKinney discovered a really great toolset built for Rails projects called JSON-API Suite.

JSON-API Suite presents the concepts of resources and serializers which are tied to each ActiveRecord model you’d like available to your API.

Our front-end engineers quickly discovered that interacting with server requests and responses formatted for the JSON-API spec was a bit of a chore: the data attributes are nested in a child object, and relationships are split into a series of sibling data objects.

Thankfully, the maintainers of the JSON-API Suite also built a project called JSORM. This JavaScript library makes it a breeze to fetch and persist models on the front-end, similar to how you might do so on the server:

Using JSORM changed our world. Querying and persisting objects felt very similar to how we might do it on the server in Rails. The only weird part about this was having to declare model and relationship behavior on both the server and on the front-end.

We had success adhering to the JSON-API spec using the JSON-API Suite, and it sounds like other companies like Netflix are experimenting with new approaches to model serialization with the fast_jsonapi gem.

What did we learn?

Our team learned a lot from building a single-page app using a modern JavaScript framework. While it probably took more time than building a simple CRUD app using Rails views, we came away with some valuable lessons:

  • Vue is really lightweight, and it’s a popular tool with a lot of third-party libraries and plugins. This makes implementing certain tools — like routing and state management — a breeze. However, since Vue is so lightweight, you might find yourself writing extra functionality for a single-page app you wouldn’t have expected to — like dynamic page titles and meta descriptions.
  • Single-file components are really great. Our team of engineers felt that Vue’s single-file component system was intuitive, and it was easy to set up a minimum-viable product with very little experience. Additionally, grouping functionality, markup and styles in a single felt more natural than splitting up a component’s functionality with a separate template file, like you might find in Glimmer. Finally, we enjoyed writing markup that was similar to HTML instead of ramping up to JSX markup we might have used had we chosen React as our framework.
  • Vue isn’t opinionated, so you have to be. Our team had a lot of discussions surrounding file structure and rules of thumb for writing components consistently. Since Vue doesn’t dictate these things out of the box, it was important for our team to be opinionated about our approach in order to reduce chaos.

Our team is excited to share the experience we had with Vue and Rails with the rest of the product team. Share your thoughts or experiences with me at @jplhomer.

This project was a joint effort between members of the Revenue Platforms team: product manager Briar Cromartie, designer Matt Sullivan, engineers Drew Amato, Becca Barton, Messay Bekele, Josh Larson, Winston Hearn, Steve McKinney, and Guillermo Esteves, and QA team Nate Edwards and Andrew Geesler.

Special thanks to Becca Barton for thoughtful edits and contributions to this post.

Our team found the following resources to be useful, especially for a first-time Vue project: