« Back to all articles

Building a web app from scratch fast using Vue.js and CQRS

Starting with our purpose

A web app can change the world

A well-built, well-tested web app can change the world. It can save lives, connect loved ones, create happiness, or save time. At it’s best, a web app can help us improve our collective society.

Our greatest nemesis

On the contrary, a poorly-developed web app could result in the opposite. Even worse, one small bug could lead to a colossal catastrophe. It could create a detrimental situation costing lives, sadness, or millions of dollars.

All of these ailments fall into the category of unintended side-effects (also known as bugs). For instance, what happens when the app fails to communicate with another app? What happens when the database fills up? What happens if a customer clicks on the wrong button? What happens if an update to the data structure fails?

Avoiding unintended side-effects

So, how can we avoid unintended side-effects? From my experience, most can be eliminated by:

  1. Establish boundaries when dealing with data – inside or outside of the app.
  2. Build a full library of specifications that describe the behaviour of the system. Run the “specs” against any new changes to the app.
  3. If you’re lucky enough to be starting an app from scratch, the most effective strategy is a well-organized code-base and data structure that is adaptive to change.

As we continue, I’ll lay out strategies, mental models, and practical tools to leverage these strategies to avoid these unintended side-effects.

Why Vue?

The examples to follow are laid out using Vue.js, but they could be applied to any other framework.

Vue is flexible at high & low levels

Nevertheless, Vue.js has been my “go-to” for software development for the last few years due to its development cycle, simplicity, and ecosystem. Despite being beginner-friendly at a high-level, Vue also gives you the knobs and switches to control your app from a low level. I highly recommend it if you’re just starting out.

Privacy-focused

Lately I’ve been particularly mindful to privacy, security, and data ownership aspects of building a web app. Generally speaking, this means building a client-side-only app. This means that the Vue app contains everything needed for the user of the software. If it is important for the app to run cross-device (or is collaborative), then I build out a server-side API as needed. Sometimes, this could be as simple as reading/writing from an S3 data store.

This philosophy aligns perfectly with Vue, and generally with the Single-Page Application trend.

No trust needed

As an added side-effect, our users will be able to fully download and host our app on a server or device that they trust.

Why CQRS?

Command-Query Responsibility Segregation is a pattern that can be used to organize a system and its flows, popularized by Martin Fowler. The largest benefit of this approach is it’s ability to eliminate side-effects.

The general idea of CQRS is to separate reading data from writing data. Whenever we need to store data, we use a Command. Whenever we need to retrieve data, we use a Query. The result is a simple data flow, which helps us to minimize the unintended side-effects of our app.

CQRS and Vue.js

Before we dig in to the code, let’s quickly break down the pieces of our approach.

User Interface (UI)

The UI is the main input and output of our web app. It is what our app user will be interacting with to accomplish their goals and see their data.

Vue.js is an absolutely perfect match with CQRS due to its performant virtual DOM hydration – as we’ll discover a little later on. That being said, the strategies outlined here could apply to any frontend framework (or system!).

State

This is the user’s data; it is a simple object store. To establish a clean boundary, the state can only be read by Queries and can only be manipulated by Commands.

Queries

These are all of our app’s “requests for data”. They are all read-only requests. Queries only interact with the State and Adapters.

Commands

Commands are the actions that can be taken in the app. They are write-only requests and don’t return anything (void). If you need to read data within a Command, use our Queries object.

The void part gets a little weird at first: “How do I get data back to the customer after they perform an action?” Because the UI is reading from the state (via Queries) and the UI is updating state (via Commands), Vue.js is able to glue it all together for us!

When writing Commands, follow the Single-Responsibility Principle and use clear names. Don’t be afraid to rename after you get the function is working and fully tested.

Adapters

When data needs to be retrieved that lives outside of our app, we need to use an Adapter. By keeping this logic out of Commands, they are kept separated from our business logic.

Another benefit of Adapters is that they can return data exactly how the app wants to digest it.

Scaffolding the App

Let’s get started building our app.

Bootstrapped

With Vue’s CLI tool, it’s a breeze. First, install Vue CLI:

yarn global add @vue/cli

Then, create an app:

$ vue create newly-created-app
? Please pick a preset: (Use arrow keys)

$ Custom ([Vue 3] dart-sass, babel, pwa, router, eslint, unit-jest) 
  Default ([Vue 2] babel, eslint) 
  Default (Vue 3) ([Vue 3] babel, eslint) 
  Manually select features 

The configuration isn’t all that important at this stage; find a configuration that suits you. If you plan to offer offline support for your web app, I recommend including pwa at the onset of the project. We’ll be using jest for tests a little later on.

After the dependencies are installed, you should have a new directory with your bootstrapped web app inside:

🎉  Successfully created project newly-created-app.
👉  Get started with the following commands:

 $ cd newly-created-app
 $ yarn serve

Folder Structure

App icons

If you have your web app icons already, now is a great time to pop them in the public folder.

Git

Pre-deploy hooks

Publishing our app

Always be releasing.

A fast deploy process increases productivity, decreases mistakes, and boosts developer happiness. Deploying a front-end-only app can be done by adding the files to an S3 bucket or even uploading via (S)FTP. There are also many third-party services that can help get your site live.

If you are already using GitHub, the quickest way to get your app published is to use GitHub Pages. To keep things organized, I usually drop a “deploy” script at ./scripts/deploy.js:

/* eslint-disable no-console */

const execa = require('execa')
const fs = require('fs')
const deployBranch = 'gh-pages'
const masterBranch = 'master'

const refresh = async () => {
  console.log('🌱 Creating fresh branch...')

  await execa('git', ['checkout', '--orphan', deployBranch])
}

const build = async () => {
  console.log('🔨 Building...')

  await execa('npm', ['run', 'build'])
  await execa('git', ['--work-tree', 'dist', 'add', '--all'])
  await execa('git', ['--work-tree', 'dist', 'commit', '-m', deployBranch, '--no-verify'])
}

const push = async () => {
  console.log('🌏 Deploying...')

  await execa('git', ['push', 'origin', `HEAD:${deployBranch}`, '--force'])
  await execa('rm', ['-r', 'dist'])
  await execa('rm', ['-rf', '.git/gc.log'])
  await execa('git', ['checkout', '-f', masterBranch])
  await execa('git', ['branch', '-D', deployBranch])
}

const done = () => {
  console.log('✅ Deployed!')
}

Promise.resolve()
  .then(refresh)
  .then(build)
  .then(push)
  .then(done)
  .catch((e) => {
    console.log(e.message)
    process.exit(1)
  })

Then, I add a shortcut in package.json:

{
  "...": "...",
  "scripts": {
    "deploy": "node scripts/deploy.js",
    "...": "..."
  }
}

Now, deploying is as simple as yarn deploy or npm run deploy:

$ yarn deploy
🌱 Creating fresh branch...
🔨 Building...
🌏 Deploying...
✅ Deployed!

Alternatively, we could deploy using GitHub Actions.

With our deploy script in place, we have no excuse not to be releasing early and often.

When deploying using a third-party host, I recommend that you keep control at the DNS layer. If anything goes wrong (eg. downtime or booted off for some reason), you can simply switch your DNS to point to another file host. In our case, it would mean setting up a subdomain the-ultimate-domain-manager.dallasread.com instead of using the default URL of dallasread.github.io/the-ultimate-domain-manager. Using a pathless URL also means Vue CLI will deploy out of the box.

A project’s path

Projects have a tendency to take on a mind of their own. It is important to be purposeful about the goal of the project from the start – as well as weighing business and marketing concerns, which is a topic for another time.

This is a work in progress; more to come…