How to route between Rails 5 API and VueJS in HTML5 mode

Keep those URLs pretty

Posted by John Thomas 14-Apr-2017

One of the nice features of vue-router is that it comes with a HTML5 history mode. The default mode is hash mode, which makes your URLs looks like this: https://example.com/#/login. That's ok, but not as nice as using the hashless https://example.com/login path. HTML5 mode allows you to do just, in a super simple way.

So, how do you enable HTML mode? Simple, just like this:

// client/src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/components/Index'
import Login from '@/components/Login'

Vue.use(Router)

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'Index',
      component: Index
    },
    {
      path: '/login',
      name: 'Login',
      component: Login
    }
  ]
})

Adding the mode: 'history' setting on the router enables the nice URL syntax.

The only big issue with HTML5 mode, is that if a user goes straight to a nested route (anything other than /), or refreshes the page on a nested route, vue-router won't be able to intercept the request and serve up the proper page. Those requests will go directly to your Rails 5 API backend. In order to get around this, you can create a redirect from your Rails API backend to VueJS with information about which page the user is really looking for. Here is and example of how your Rails router might look:

# config/routes.rb

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      # v1 api routes here
    end
  end

  match "/*path", to: redirect("/?redirect=%{path}"), via: :all
end

That last line inside the routes block will redirect to the / route (which vue is mounted on) with information about the requested resource (/?redirect=%{path}). For example, that last line would redirect a GET https://example.com/login request to a GET https://example.com/?redirect=login

Now that we have given vue-router all the information it needs, we can add a path that will load the proper page:

// client/src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/components/Index'
import Login from '@/components/Login'
import NotFound from '@/components/NotFound'

Vue.use(Router)

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'Redirect',
      // vue-router lets us define a redirect method, the target route `to` is available for the redirect function
      // https://router.vuejs.org/en/essentials/redirect-and-alias.html
      redirect: function (to) {
        if (to.query.redirect) {
          // This will clear the ?redirect=<path> from the end URL
          var path = to.query.redirect
          delete to.query.redirect
          return {
            path: '/' + path,
            query: to.query
          }
        } else {
          return {
            path: '/index',
            query: to.query
          }
        }
      }
    },
    {
      path: '/index',
      name: 'Index',
      component: Index
    },
    {
      path: '/login',
      name: 'Login',
      component: Login
    },
    {
      path: '*',
      name: 'NotFound',
      component: NotFound
    }
  ]
})

There it is, a working redirect solution between your Rails API and VueJS front-end using HTML5 navigation mode. Now, there are a couple things to note here:

  • We had to move the "Index" component off the '/' path. In my example above I moved it to '/index'. This means that if someone goes to the homepage, the router will redirect to https://example.com/index. I don't love that, but I could not find a way to render a component from within the redirect function. If you know of a way, please tweet at me.

  • We delete the redirect query parameters on redirect. For example, if you didn't delete that query parmeter, and visited https://example.com/?redirect=login, you would be redirected to https://example.com/login?redirect=login. This cleans up pure redirects while still preserving any other query parameters.

  • I have also added a NotFound component that will render a 404 page if someone tries to redirect to a page that doesn't exist. For example, if a user went to https://example.com/?redirect=not-really-a-page. they would hit the Redirect route, which would send them to https://example.com/not-really-a-page, which would get matched to the path: '*' matcher and render the 404 page.