Ruby on Rails API with Vue.js
Tags: vue, rails, javascript, tutorial
Published: 2019-04-10 03:07:27

---
title: Ruby on Rails API with Vue.js
published: true
tags: vuejs, rails, javascript, tutorial
canonical_url: https://web-crunch.com/ruby-on-rails-api-vue-js/
cover_image: https://thepracticaldev.s3.amazonaws.com/i/rce92fk3ols5po1ug3u4.jpg
---

Did you know Ruby on Rails can be used as a strict API based backend application? What’s the benefit to this? Think of it as a single source of truth for multiple _future_ applications to absorb and use this data directly. Anything from a native mobile application, to a front-end framework, can talk with this data. Many apps can essentially communicate with a “source of truth” in return which means more consistent applications for all.

In this build, I’ll be crafting a simple but thorough application where Ruby on Rails is our backend and Vue.js + Axios is our front-end. I’ll create two apps that communicate in order to achieve the same result of a normal Rails-based app but with all the perks of an API.

#### Used in this build

- `Rails 5.2.2`
- `Ruby 2.5`
- Gem `bcrypt 3.1.7`
- Gem `rack-cors`
- Gem [`redis 4.1.0`](https://github.com/redis/redis-rb)
- Gem [`jwt-sessions`](https://github.com/tuwukee/jwt_sessions)

### What are we building exactly?

This app at its core is simple. It will be an archive of vinyl records for sale and categorized by artist. We won’t be implementing a ton of foreign logic but rather just getting the foundations of an API-based application in order. We’ll touch on authentication (not using Devise) and basic CRUD.

There will be two apps.

- A Ruby on Rails backend – This will handle our data, sessions, and authentication.
- A Vue.js frontend – This will be the view layer but also the one responsible for sending and receiving data to our rails-based backend. The front-end will run on a different instance using the [Vue-CLI](https://cli.vuejs.org/) to help us set up an app.

### The videos

#### Part 1

{% youtube eUedqaHS4RQ %}

#### Part 2

{% youtube TBZwkWznhZs %}

#### Part 3

{% youtube Pa6B0J_l8oc %}

#### Part 4

{% youtube 4e-ZlVsT5z4 %}

#### Part 5

{% youtube o_z5pol6vZE %}

#### Part 6

{% youtube d9_-8yZmOF4 %}

#### Part 7

{% youtube VqhcKlee2L0 %}

#### Part 8

{% youtube zFgoIPNBddc %}

## The Backend

Our backend will be a very trimmed down Rails app with no view-based layer. Rails has a handy `api` mode which you can initialize by passing the flag `--api` during the creation of a new app. Let’s dive in.

### Create the app in API mode

```bash
$ rails new recordstore-back --api
```

#### Add gems

1. Uncomment `rack-cors` and `bcrypt` .
2. add `redis` and `jwt_sessions`
3. `bundle install`

Here’s the current state of my `Gemfile`

```ruby
# Gemfile - Jan 2019
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.5.3'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.2.2'
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
# Use Puma as the app server
gem 'puma', '~> 3.11'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
# gem 'jbuilder', '~> 2.5'
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'

# Use ActiveStorage variant
# gem 'mini_magick', '~> 4.8'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.1.0', require: false

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem 'rack-cors'
gem 'redis', '~> 4.1'
gem 'jwt_sessions', '~> 2.3'

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
  gem 'listen', '>= 3.0.5', '< 3.2'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
```

### Create a User model

We won’t be using Devise this time around! Rails has some handy built-ins to help users set up authentication. This route is certainly more involved but I recommend doing this to learn more about how popular gems like Devise work (and solve a lot of headaches).

To avoid too much complexity upfront our `User` model won’t associate with the `Record` or `Artist` model just yet. Later we can add that so a `User` can add both an `Artist` and `Record` to the app with the front-end interface.

```bash
$ rails g model User email:string password_digest:string
```

The `password_digest` field will make use of the `bcrypt` gem we uncommented during initial setup. It creates a tokenized version of your password for better security.

We’ll need to modify the migration file to include a default of `null: false` on the `email` and`password_digest` columns.

```ruby
# db/migrate/20190105164640_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :email, null: false
      t.string :password_digest, null: false

      t.timestamps
    end
  end
end
```

Let’s migrate that in

```bash
$ rails db:migrate
```

### Create an Artist Model

The `Artist` model will be the parent relation in our app. A record (soon to come) will belong to an artist

```bash
$ rails g scaffold Artist name
```

Notice how no views are created when that resource gets scaffolded? That’s again our API-mode at work. Our controllers also render `JSON` but default.

### Create a Record Model

Our `Record` model will have a few more fields and belong to an artist. This scaffold creates a `Record` model (class) that has `title`, `year`, `artist_id` and `user_id` columns on the new `records` database table. This creates a new migration with all of this data in mind. 


```bash
$ rails g scaffold Record title year artist:references user:references
```

Migrate both models in

```bash
$ rails db:migrate
```

### Namespacing our API

Having scaffolded the models and data structures we need let’s talk routing. APIs often change. A common trend is to introduce versions which allow third-parties to opt into a new API version when they see fit. Doing this presents fewer errors for everyone but comes with a little more setup on the backend which mostly deals with routing and file location.

To namespace our app I want to do a `v1` type of concept which ultimately looks like this:

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

Namespacing allows us to extend things further at any point say if we roll out a new version or decide to build more with the backend. All of our data will live within the namespace but our user-related data won’t. We probably won’t change a lot with the userbase on the backend that would need to be in an API. Your results may vary as your app scales.

### Update the routes

Next, we need to add our recently scaffolded resources to the mix

```ruby
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :artists
      resources :records
    end
  end
end
```

Having updated our namespacing we need to move our controllers to accommodate. Move `artists_controller.rb` and `records_controller.rb` to `app/controllers/api/v1/` . Be sure to modify both to include the new namespacing like so. By the way, If your server was running you should restart it.

Here is the artists controller:

```ruby
# app/controllers/api/v1/artists_controller.rb
module Api
  module V1
    class ArtistsController < ApplicationController
      before_action :set_artist, only: [:show, :update, :destroy]

      def index
        @artists = Artist.all

        render json: @artists
      end

      def show
        render json: @artist
      end

      def create
        @artist = Artist.new(artist_params)

        if @artist.save
          render json: @artist, status: :created
        else
          render json: @artist.errors, status: :unprocessable_entity
        end
      end

      def update
        if @artist.update(artist_params)
          render json: @artist
        else
          render json: @artist.errors, status: :unprocessable_entity
        end
      end

      def destroy
        @artist.destroy
      end

      private
      def set_artist
          @artist = Artist.find(params[:id])
      end

      def artist_params
          params.require(:artist).permit(:name)
      end
    end
  end
end
```

And here’s the `records_controller.rb` file

```ruby
module Api
  module V1
    class RecordsController < ApplicationController
      before_action :set_record, only: [:show, :update, :destroy]

      def index
        @records = current_user.records.all

        render json: @records
      end

      def show
        render json: @record
      end

      def create
        @record = current_user.records.build(record_params)

        if @record.save
          render json: @record, status: :created
        else
          render json: @record.errors, status: :unprocessable_entity
        end
      end

      def update
        if @record.update(record_params)
          render json: @record
        else
          render json: @record.errors, status: :unprocessable_entity
        end
      end

      def destroy
        @record.destroy
      end

      private
      def set_record
        @record = current_user.records.find(params[:id])
      end

      def record_params
        params.require(:record).permit(:title, :year, :artist_id)
      end
    end
  end
end
```

### Getting JWT\_Sessions Setup

JSON Web Tokens are how we will handle authentication in this app. Rails apps that aren’t API-based use session-based tokens to verify logins/sessions of a given `User`. We don’t have the same session logic available to do such a thing with an API driven frontend app. We also want our API available to other applications or things we build like a mobile app, native app, and more (the possibilities are kinda endless). This concept is why API-based applications are all the craze.

Let’s setup JWTSessions.

```ruby
# app/controllers/application_controller.rb

class ApplicationController < ActionController::API
   include JWTSessions::RailsAuthorization
end
```

Inside your `application_controller.rb` file add the following include. We get this from the gem we installed previously.

Note how your controller inherits from `ActionController::API` instead of the default `ApplicationController`. That’s the `API` mode in full force!

We need some exception handling for unauthorized requests. Let’s extend the file to the following:

```ruby
# app/controllers/application_controller.rb

class ApplicationController < ActionController::API
  include JWTSessions::RailsAuthorization
  rescue_from JWTSessions::Errors::Unauthorized, with :not_authorized

  private

  def not_authorized
    render json: { error: 'Not Authorized' }, status: :unauthorized
  end
end
```

We’ll also need an encryption key. The JWTSessions gem by default uses `HS256` algorithm, and it needs an encryption key provided.

The gem uses Redis as a token store by default so that’s why you saw it in our `Gemfile`. We need a working `redis-server` instance running. It is possible to use local memory for testing but we’ll be using `redis` for this build as it’s what would run in production anyway. [Check out the readme for more information](https://github.com/tuwukee/jwt_sessions#token-store)

Create a new initializer file called `jwt_sessions.rb` and add the following

```ruby
# config/initializers/jwt_sessions.rb

JWTSessions.encryption_key = 'secret' # use something else here
```

Definitely worth using something other than your secret key here if you prefer!

### Signup Endpoint

Because we are going the token-based route we can choose to either store those on the client side cookies or localStorage. It boils down to preference where you land. Either choice has its pros and cons. Cookies being vulnerable to CSRF and localStorage being vulnerable to XSS attacks.

The `JWT_Sessions` gem provides the set of tokens – access, refresh, and CSRF for cases when `cookies` are chosen as the token store option.

We’ll be making use of cookies with CSRF validations

The session within the gem comes as a pair of tokens called `access` and `refresh`. The access token has a shorter life span with a default of 1 hour. Refresh on the other hand has a longer life span of ~ 2 weeks. All of which is configurable.

We’ll do quite a bit of logic in a `signup_controller` file of which we can generate.

```bash
$ rails g controller signup create
```

For now we can omit the route that gets generated in `config/routes.rb`

```ruby
Rails.application.routes.draw do
    get 'signup/create' # remove this line
    ...
end
```

Let’s add the logic for signup to the controller. We’ll be harnessing the JWT\_Sessions gem for this.

```ruby
# app/controllers/signup_controller.rb

class SignupController < ApplicationController
  def create
    user = User.new(user_params)
    if user.save
      payload = { user_id: user.id }
      session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
      tokens = session.login

      response.set_cookie(JWTSessions.access_cookie,
                          value: tokens[:access],
                          httponly: true,
                          secure: Rails.env.production?)
      render json: { csrf: tokens[:csrf] }
    else
      render json: { error: user.errors.full_messages.join(' ') }, status: :unprocessable_entity
    end
  end

  private

  def user_params
    params.permit(:email, :password, :password_confirmation)
  end
end
```

A lot is going on here but it’s not _too_ impossible to understand. We’ll point the user to the endpoint `signup/create` method. In doing so we accomplish the following if all goes well.

- Create a new user with permitted parameters (email, password, password\_confirmation)
- Assign the user\_id as the payload
- Create a new token-based session using the payload & JWTSessions.
- Set a cookie with our JWTSession token [:access] 
- render final JSON & CSRF tokens to avoid cross-origin request vulnerabilities.
- If none of that works we render the errors as JSON 

### Signin/Signout Endpoint

The Sign in Controller is quite similar to the signup minus the creation of a user and what happens if a user can’t sign in successfully. There’s the `create` method but also a `destroy` method for signing a user out.

```ruby
# app/controllers/signin_controller.rb

aclass SigninController < ApplicationController
  before_action :authorize_access_request!, only: [:destroy]

  def create
    user = User.find_by!(email: params[:email])
    if user.authenticate(params[:password])
      payload = { user_id: user.id }
      session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
      tokens = session.login
      response.set_cookie(JWTSessions.access_cookie,
                        value: tokens[:access],
                        httponly: true,
                        secure: Rails.env.production?)
      render json: { csrf: tokens[:csrf] }
    else
      not_authorized
    end
  end

  def destroy
    session = JWTSessions::Session.new(payload: payload)
    session.flush_by_access_payload
    render json: :ok
  end

  private

  def not_found
    render json: { error: "Cannot find email/password combination" }, status: :not_found
  end
end
```

We render the `not_authorized` method which comes from our Application controller private methods if a sign in is unsuccessful.

### The Refresh Endpoint

Sometimes it’s not secure enough to store the refresh tokens in web / JS clients. We can operate with token-only with the help of the `refresh_by_access_allowed` method you’ve been seeing so far. This links the access token to the refresh token and refreshes it.

Create a `refresh_controller.rb` file and include the following:

```ruby
# app/controllers/refresh_controller.rb
class RefreshController < ApplicationController
  before_action :authorize_refresh_by_access_request!

  def create
    session = JWTSessions::Session.new(payload: claimless_payload, refresh_by_access_allowed: true)
    tokens = session.refresh_by_access_payload do
      raise JWTSessions::Errors::Unauthorized, "Somethings not right here!"
    end
    response.set_cookie(JWTSessions.access_cookie,
                        value: tokens[:access],
                        httponly: true,
                        secure: Rails.env.production?)
    render json: { csrf: tokens[:csrf] }
  end
end
```

Here I’m expecting only expired access tokens to be used for a refresh so within the `refresh_by_access_payload` method we added an exception. We could do more here like send a notification, flush the session, or ignore it altogether.

The JWT library checks for expiration claims automatically. To avoid the except for an expired access token we can harness the `claimless_payload` method.

The `before_action :authorized_refresh_by_access_request!` is used as a protective layer to protect the endpoint.

### Update controllers to add access request

Much like Devise’s built-in `authorize_user!` method we can use one from JWT on our controllers.

```ruby
# app/controllers/api/v1/artists_controller.rb

module Api
  module V1
    class ArtistsController < ApplicationController
        before_action :authorize_access_request!, except: [:show, :index]
      ...
      end
   end
  end
end
```

And our records controller:

```ruby
# app/controllers/api/v1/records_controller.rb

module Api
  module V1
    class RecordsController < ApplicationController
        before_action :authorize_access_request!, except: [:show, :index]
      ...
      end
   end
  end
end
```

### Creating `current_user`

Again much like Devise we want a helper for the given user who is logged in. We’ll have to establish this ourselves inside the application controller.

```ruby
# app/controllers/application_controller.rb

class ApplicationController < ActionController::API
  include JWTSessions::RailsAuthorization
  rescue_from JWTSessions::Errors::Unauthorized, with: :not_authorized

  private

  def current_user
    @current_user ||= User.find(payload['user_id'])
  end

  def not_authorized
    render json: { error: 'Not authorized' }, status: :unauthorized
  end
end
```

### Making sure we can authorize certain Cross-Origin requests

Ruby on Rails comes with a `cors.rb` file within `config/initializers/`. If you don't see one feel free to create it. Every file within `config/initializers` gets autoloaded. 

Inside that file we can specify specific origins to allow to send/receive requests. Our front-end will run on a different local server so this is where we could pass that. When your app is live you’ll probably point this to a living domain/subdomain.

If you haven’t already, be sure to add/uncomment `rack-cors` in your `Gemfile` and run `bundle install`. Restart your server as well if it is running.

```ruby
# config/initializers/cors.rb

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:8081'

    resource '*',
      headers: :any,
      credentials: true,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
```

Your origin will be whatever your frontend port is running on. In my case, it’s `8081`. You can comma separate more origins to allow secure access.

### Moar Routing!

With all of our endpoints defined we can add those to our routes outside of our API namespaces. My current routes file looks like the following:

```ruby
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :artists do
        resources :records
      end
    end
  end

  post 'refresh', controller: :refresh, action: :create
  post 'signin', controller: :signin, action: :create
  post 'signup', controller: :signup, action: :create
  delete 'signin', controller: :signin, action: :destroy
end
```

We can define the request, controller, name of URL path, and action to fire all in one line of ruby. Love it!

### Data

Create some test data in the rails console by running `rails c` in your terminal. I’ll create a few artists at random just so we have some data to display when testing out our front-end app coming up.

```bash
Artist.create!(name: "AC/DC")
Artist.create!(name: "Jimi Hendrix")
Artist.create!(name: "Alice in Chains")
....
# repeat for however many artists you would like to add
```

## The Frontend

Let’s adopt Vue.js for the frontend and tackle that portion of the build. This app will live within the rails app but run separately altogether. Rather than keeping source code separate we can house it within a root folder in our app.

Our toolbox will consist of [Node.js](https://nodejs.org/en/), [VueJS CLI](https://cli.vuejs.org/), [Yarn](https://yarnpkg.com/en/) and [Axios](https://github.com/axios/axios).

If you’re new to Vue this might be a little overwhelming to grasp at first but it’s quite a convention driven like Rails. The fact that you can sprinkle it throughout any type of app sold me as opposed to frameworks like Angular or React.

At the time of this writing/recording I’m using the following version of node:

```bash
$ node -v
v11.4.0
$ yarn -v
1.12.3
```

#### Install Vue CLI

```bash
$ yarn global add @vue/cli
```

`global` means this installs at the system level rather than directly in your project `node_modules` though still depends on them.

We can check the version of `vue` to verify install

```bash
$ vue --version
2.9.6
```

#### Create a new project

`cd` into your rails app if you haven’t already and run the following:

```bash
$ vue init webpack recordstore-front
```

This will ask a slew of questions. Here are my responses if you’re wanting to follow along:

```bash
? Project name recordstore-front
? Project description A Vue.js front-end app for a Ruby on Rails backend app.
? Author Andy Leverenz <andy@web-crunch.com>
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Set up unit tests Yes
? Pick a test runner karma
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recommended) yarn
```

### Starting the app

```bash
$ cd recordstore-front
$ yarn dev
```

Webpack should do its magic here and you should be able to open your browser to see the new Vue app on `localhost:8081`

My working directory looks like this:

```bash
$ tree . -I "node_modules"
.
├── README.md
├── build
│   ├── build.js
│   ├── check-versions.js
│   ├── logo.png
│   ├── utils.js
│   ├── vue-loader.conf.js
│   ├── webpack.base.conf.js
│   ├── webpack.dev.conf.js
│   ├── webpack.prod.conf.js
│   └── webpack.test.conf.js
├── config
│   ├── dev.env.js
│   ├── index.js
│   ├── prod.env.js
│   └── test.env.js
├── index.html
├── package.json
├── src
│   ├── App.vue
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── HelloWorld.vue
│   ├── main.js
│   └── router
│   └── index.js
├── static
├── test
│   └── unit
│   ├── index.js
│   ├── karma.conf.js
│   └── specs
│   └── HelloWorld.spec.js
└── yarn.lock

10 directories, 25 files
```

**Note** : if you want `tree` to work on your system you’ll need to install it. I used homebrew and ran the following:

```bash
$ brew install tree
```

### Add Tailwind CSS

#### Installing Tailwind CSS

Note: [A new Tailwind exists today in beta form](https://next.tailwindcss.com). Feel free to use it instead. 

I’ve been loving Tailwind so I’m adding it to my project. You can use something more complete like Bootstrap and simply link it via CDN but like I said Tailwind is pretty sweet. I’ll add it with Yarn

```bash
$ yarn add tailwindcss --dev
```

Per the tailwind docs we need to run and `init` command directly from the node\_modules folder

```bash
$ ./node_modules/.bin/tailwind init
   tailwindcss 0.7.3
   ✅ Created Tailwind config file: tailwind.js
```

A `tailwind.js` file should appear in your project ready to configure.

#### Add a CSS file

Our CSS will compile down but we need it to have a place for it to do so. In our `src` directory add a `main.css` file.

```bash
src/
 assets/
 components/
 routes/
 App.vue
 main.js
 main.css
```

Insie `main.css` we need the following:

```css
/* recordstore-frontend/src/main.css */

@tailwind preflight;

@tailwind components;

@tailwind utilities;
```

In `main.js` add the following

```javascript
// recordstore-frontend/src/main.js
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import './main.css'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})
```

Almost done we just need to tell our app about `tailwind.js`

### PostCSS config

We need to declare tailwind as a plugin in our `.postcss.config.js` file and configure purge css as well.

```javascript
// recordstore-frontend/.postcss.config.js

module.exports = {
  "plugins": {
    "postcss-import": {},
    "tailwindcss": "./tailwind.js",
    "autoprefixer": {}
  }
}
```

### Cleanup

I’ll remove the default `HelloWorld` component from `src/components` and the line referencing it inside `main.js`

### Install and Configure Axios

```bash
$ yarn add axios vue-axios
```

Having installed both of those packages I’ll make a home for our axios internals

Create a new folder called `backend` within `src` Within that folder create a folder called `axios` and finally inside that create an `index.js` file. Here we’ll give axios some global defaults and assign our API URL as a constant which gets used throughout each request.

```javascript
// recordstore-frontend/src/backend/axios/index.js

import axios from 'axios'

const API_URL = 'http://localhost:3000'

const securedAxiosInstance = axios.create({
  baseURL: API_URL,
  withCredentials: true,
  headers: {
    'Content-Type': 'application/json'
  }
})

const plainAxiosInstance = axios.create({
  baseURL: API_URL,
  withCredentials: true,
  headers: {
    'Content-Type': 'application/json'
  }
})

securedAxiosInstance.interceptors.request.use(config => {
  const method = config.method.toUpperCase()
  if (method !== 'OPTIONS' && method !== 'GET') {
    config.headers = {
      ...config.headers,
      'X-CSRF-TOKEN': localStorage.csrf
    }
  }
  return config
})

securedAxiosInstance.interceptors.response.use(null, error => {
  if (error.response && error.response.config && error.response.status === 401) {
    // If 401 by expired access cookie, we do a refresh request
    return plainAxiosInstance.post('/refresh', {}, { headers: { 'X-CSRF-TOKEN': localStorage.csrf } })
      .then(response => {
        localStorage.csrf = response.data.csrf
        localStorage.signedIn = true
        // After another successfull refresh - repeat original request
        let retryConfig = error.response.config
        retryConfig.headers['X-CSRF-TOKEN'] = localStorage.csrf
        return plainAxiosInstance.request(retryConfig)
      }).catch(error => {
        delete localStorage.csrf
        delete localStorage.signedIn
        // redirect to signin if refresh fails
        location.replace('/')
        return Promise.reject(error)
      })
  } else {
    return Promise.reject(error)
  }
})

export { securedAxiosInstance, plainAxiosInstance }
```

The gist of what we just did is that axios doesn’t have all the logic we were after. We built two wrappers around axios to get what we desire. We are passing through credentials that check against our CSRF tokens from Rails. In doing so we can establish some logic on if the right criteria are met to log the user in and out, send the right data, and more.

### Main Vue configuration

The `main.js`file is our next stop. We’ll import our dependencies and configure a bit more:

```javascript
// recordstore-frontend/src/main.js

import Vue from 'vue'
import App from './App'
import router from './router'
import VueAxios from 'vue-axios'
import { securedAxiosInstance, plainAxiosInstance } from './backend/axios'
import './main.css' // tailwind

Vue.config.productionTip = false
Vue.use(VueAxios, {
  secured: securedAxiosInstance,
  plain: plainAxiosInstance
})

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  securedAxiosInstance,
  plainAxiosInstance,
  components: { App },
  template: '<App/>'
})
```

Notice how we make use of VueAxios, and our new `secured` and `plain` instances. Think of these as scoped logic which we will use during runtime on our Vue components. You’ll see how this works coming up when we create each component.

### Routing on the frontend

I’ll start with the signin component we’ve been building but focus on the front-end routing using Vue router.

```javascript
// recordstore-frontend/router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import Signin from '@/components/Signin'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Signin',
      component: Signin
    }
  ]
})
```

### Build the `Signin` Vue Component

```javascript
<!-- recordstore-frontend/src/components/Signin.vue -->

<template>
  <div class="max-w-sm m-auto my-8">
    <div class="border p-10 border-grey-light shadow rounded">
      <h3 class="text-2xl mb-6 text-grey-darkest">Sign In</h3>
      <form @submit.prevent="signin">
        <div class="text-red" v-if="error">{{ error }}</div>

        <div class="mb-6">
          <label for="email" class="label">E-mail Address</label>
          <input type="email" v-model="email" class="input" id="email" placeholder="andy@web-crunch.com">
        </div>
        <div class="mb-6">
          <label for="password" class="label">Password</label>
          <input type="password" v-model="password" class="input" id="password" placeholder="Password">
        </div>
        <button type="submit" class="font-sans font-bold px-4 rounded cursor-pointer no-underline bg-green hover:bg-green-dark block w-full py-4 text-white items-center justify-center">Sign In</button>

        <div class="my-4"><router-link to="/signup" class="link-grey">Sign up</router-link></div>
      </form>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Signin',
  data () {
    return {
      email: '',
      password: '',
      error: ''
    }
  },
  created () {
    this.checkSignedIn()
  },
  updated () {
    this.checkSignedIn()
  },
  methods: {
    signin () {
      this.$http.plain.post('/signin', { email: this.email, password: this.password })
        .then(response => this.signinSuccessful(response))
        .catch(error => this.signinFailed(error))
    },
    signinSuccessful (response) {
      if (!response.data.csrf) {
        this.signinFailed(response)
        return
      }
      localStorage.csrf = response.data.csrf
      localStorage.signedIn = true
      this.error = ''
      this.$router.replace('/records')
    },
    signinFailed (error) {
      this.error = (error.response && error.response.data && error.response.data.error) || ''
      delete localStorage.csrf
      delete localStorage.signedIn
    },
    checkSignedIn () {
      if (localStorage.signedIn) {
        this.$router.replace('/records')
      }
    }
  }
}
</script>
```

This component is a basic login form with a link to our sign up form if you don’t already have an account. We leverage Tailwind for styles and Vue for functionality. In the `script` block I check if the user is already signed in upon component creation if so they will redirect to `/records` and if not they’ll see this form. Our actual `signin` method performs a post request when the form submission is triggered.

### `Signup` Component

```javascript
<!-- recordstore-frontend/src/components/Signup.vue -->

<template>
  <div class="max-w-sm m-auto my-8">
    <div class="border p-10 border-grey-light shadow rounded">
      <h3 class="text-2xl mb-6 text-grey-darkest">Sign Up</h3>
      <form @submit.prevent="signup">
        <div class="text-red" v-if="error">{{ error }}</div>

        <div class="mb-6">
          <label for="email" class="label">E-mail Address</label>
          <input type="email" v-model="email" class="input" id="email" placeholder="andy@web-crunch.com">
        </div>

        <div class="mb-6">
          <label for="password" class="label">Password</label>
          <input type="password" v-model="password" class="input" id="password" placeholder="Password">
        </div>

        <div class="mb-6">
          <label for="password_confirmation" class="label">Password Confirmation</label>
          <input type="password" v-model="password_confirmation" class="input" id="password_confirmation" placeholder="Password Confirmation">
        </div>
        <button type="submit" class="font-sans font-bold px-4 rounded cursor-pointer no-underline bg-green hover:bg-green-dark block w-full py-4 text-white items-center justify-center">Sign Up</button>

        <div class="my-4"><router-link to="/" class="link-grey">Sign In</router-link></div>
      </form>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Signup',
  data () {
    return {
      email: '',
      password: '',
      password_confirmation: '',
      error: ''
    }
  },
  created () {
    this.checkedSignedIn()
  },
  updated () {
    this.checkedSignedIn()
  },
  methods: {
    signup () {
      this.$http.plain.post('/signup', { email: this.email, password: this.password, password_confirmation: this.password_confirmation })
        .then(response => this.signupSuccessful(response))
        .catch(error => this.signupFailed(error))
    },
    signupSuccessful (response) {
      if (!response.data.csrf) {
        this.signupFailed(response)
        return
      }

      localStorage.csrf = response.data.csrf
      localStorage.signedIn = true
      this.error = ''
      this.$router.replace('/records')
    },
    signupFailed (error) {
      this.error = (error.response && error.response.data && error.response.data.error) || 'Something went wrong'
      delete localStorage.csrf
      delete localStorage.signedIn
    },
    checkedSignedIn () {
      if (localStorage.signedIn) {
        this.$router.replace('/records')
      }
    }
  }
}
</script>
```

Much of the logic is the same for the `Signup.vue` component. Here we introduce a new field and different `POST` route on the `signup` path. This points to `/signup` on our rails app as defined in `config/routes.rb`.

### `Header.vue` component

I want to have a global header component above our router. In doing so we need to import that into our main `App.vue` file. In the end the `Header.vue` file looks like the following:

```javascript
<!-- recordstore-frontend/src/components/Header.vue -->

<template>
  <header class="bg-grey-lighter py-4">
    <div class="container m-auto flex flex-wrap items-center justify-end">
      <div class="flex-1 flex items-center">
        <svg class="fill-current text-indigo" viewBox="0 0 24 24" width="24" height="24"><title>record vinyl</title><path d="M23.938 10.773a11.915 11.915 0 0 0-2.333-5.944 12.118 12.118 0 0 0-1.12-1.314A11.962 11.962 0 0 0 12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12c0-.414-.021-.823-.062-1.227zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-5a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"></path></svg>

        <a href="/" class="uppercase text-sm font-mono pl-4 font-semibold no-underline text-indigo-dark hover:text-indigo-darker">Record Store</a>
      </div>
      <div>
        <router-link to="/" class="link-grey px-2 no-underline" v-if="!signedIn()">Sign in</router-link>
        <router-link to="/signup" class="link-grey px-2 no-underline" v-if="!signedIn()">Sign Up</router-link>
        <router-link to="/records" class="link-grey px-2 no-underline" v-if="signedIn()">Records</router-link>
        <router-link to="/artists" class="link-grey px-2 no-underline" v-if="signedIn()">Artists</router-link>
        <a href="#" @click.prevent="signOut" class="link-grey px-2 no-underline" v-if="signedIn()">Sign out</a>
      </div>
    </div>
  </header>
</template>

<script>
export default {
  name: 'Header',
  created () {
    this.signedIn()
  },
  methods: {
    setError (error, text) {
      this.error = (error.response && error.response.data && error.response.data.error) || text
    },
    signedIn () {
      return localStorage.signedIn
    },
    signOut () {
      this.$http.secured.delete('/signin')
        .then(response => {
          delete localStorage.csrf
          delete localStorage.signedIn
          this.$router.replace('/')
        })
        .catch(error => this.setError(error, 'Cannot sign out'))
    }
  }
}
</script>
```

This file get’s imported here:

```javascript
<!-- src/components/App.vue-->
<template>
  <div id="app">
    <Header/>
    <router-view></router-view>
  </div>
</template>

<script>
import Header from './components/Header.vue'

export default {
  name: 'App',
  components: {
    Header
  }
}
</script>
```

## Artists

We have data already in the database so let’s start with our `Artists.vue` component

```javascript
<!-- recordstore-frontend/src/components/artists/Artists.vue -->

<template>
  <div class="max-w-md m-auto py-10">
    <div class="text-red" v-if="error">{{ error }}</div>
    <h3 class="font-mono font-regular text-3xl mb-4">Add a new artist</h3>
    <form action="" @submit.prevent="addArtist">
      <div class="mb-6">
        <input class="input"
          autofocus autocomplete="off"
          placeholder="Type an arist name"
          v-model="newArtist.name" />
      </div>
      <input type="submit" value="Add Artist" class="font-sans font-bold px-4 rounded cursor-pointer no-underline bg-green hover:bg-green-dark block w-full py-4 text-white items-center justify-center" />
    </form>

    <hr class="border border-grey-light my-6" />

    <ul class="list-reset mt-4">
      <li class="py-4" v-for="artist in artists" :key="artist.id" :artist="artist">

        <div class="flex items-center justify-between flex-wrap">
          <p class="block flex-1 font-mono font-semibold flex items-center ">
            <svg class="fill-current text-indigo w-6 h-6 mr-2" viewBox="0 0 20 20" width="20" height="20"><title>music artist</title><path d="M15.75 8l-3.74-3.75a3.99 3.99 0 0 1 6.82-3.08A4 4 0 0 1 15.75 8zm-13.9 7.3l9.2-9.19 2.83 2.83-9.2 9.2-2.82-2.84zm-1.4 2.83l2.11-2.12 1.42 1.42-2.12 2.12-1.42-1.42zM10 15l2-2v7h-2v-5z"></path></svg>
            {{ artist.name }}
          </p>

          <button class="bg-tranparent text-sm hover:bg-blue hover:text-white text-blue border border-blue no-underline font-bold py-2 px-4 mr-2 rounded"
          @click.prevent="editArtist(artist)">Edit</button>

          <button class="bg-transprent text-sm hover:bg-red text-red hover:text-white no-underline font-bold py-2 px-4 rounded border border-red"
         @click.prevent="removeArtist(artist)">Delete</button>
        </div>

        <div v-if="artist == editedArtist">
          <form action="" @submit.prevent="updateArtist(artist)">
            <div class="mb-6 p-4 bg-white rounded border border-grey-light mt-4">
              <input class="input" v-model="artist.name" />
              <input type="submit" value="Update" class=" my-2 bg-transparent text-sm hover:bg-blue hover:text-white text-blue border border-blue no-underline font-bold py-2 px-4 rounded cursor-pointer">
            </div>
          </form>
        </div>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'Artists',
  data () {
    return {
      artists: [],
      newArtist: [],
      error: '',
      editedArtist: ''
    }
  },
  created () {
    if (!localStorage.signedIn) {
      this.$router.replace('/')
    } else {
      this.$http.secured.get('/api/v1/artists')
        .then(response => { this.artists = response.data })
        .catch(error => this.setError(error, 'Something went wrong'))
    }
  },
  methods: {
    setError (error, text) {
      this.error = (error.response && error.response.data && error.response.data.error) || text
    },
    addArtist () {
      const value = this.newArtist
      if (!value) {
        return
      }
      this.$http.secured.post('/api/v1/artists/', { artist: { name: this.newArtist.name } })

        .then(response => {
          this.artists.push(response.data)
          this.newArtist = ''
        })
        .catch(error => this.setError(error, 'Cannot create artist'))
    },
    removeArtist (artist) {
      this.$http.secured.delete(`/api/v1/artists/${artist.id}`)
        .then(response => {
          this.artists.splice(this.artists.indexOf(artist), 1)
        })
        .catch(error => this.setError(error, 'Cannot delete artist'))
    },
    editArtist (artist) {
      this.editedArtist = artist
    },
    updateArtist (artist) {
      this.editedArtist = ''
      this.$http.secured.patch(`/api/v1/artists/${artist.id}`, { artist: { title: artist.name } })
        .catch(error => this.setError(error, 'Cannot update artist'))
    }
  }
}
</script>
```

This component is responsible for a few things. I realize this could be condensed down further to multiple components but for the sake of time, I contained everything. In this file, we have a form, a listing of artists, and an update form when editing an artist. We’ll loop through the data from our Rails app to display data in the database and use Vue to perform basic CRUD operations with JavaScript and Axios.

Note how I point to `api/v1/artists` in a lot of axios requests. This is the namespace in full effect we created prior on the rails application. Cool stuff!

### The `Records.vue` component

```javascript
<!-- recordstore-frontend/src/components/artists/Records.vue -->

<template>
  <div class="max-w-md m-auto py-10">
    <div class="text-red" v-if="error">{{ error }}</div>
    <h3 class="font-mono font-regular text-3xl mb-4">Add a new record</h3>
    <form action="" @submit.prevent="addRecord">
      <div class="mb-6">
        <label for="record_title" class="label">Title</label>
        <input
          id="record_title"
          class="input"
          autofocus autocomplete="off"
          placeholder="Type a record name"
          v-model="newRecord.title" />
      </div>

      <div class="mb-6">
        <label for="record_year" class="label">Year</label>
        <input
          id="record_year"
          class="input"
          autofocus autocomplete="off"
          placeholder="Year"
          v-model="newRecord.year"
        />
       </div>

      <div class="mb-6">
        <label for="artist" class="label">Artist</label>
        <select id="artist" class="select" v-model="newRecord.artist">
          <option disabled value="">Select an artist</option>
          <option :value="artist.id" v-for="artist in artists" :key="artist.id">{{ artist.name }}</option>
        </select>
        <p class="pt-4">Don't see an artist? <router-link class="text-grey-darker underline" to="/artists">Create one</router-link></p>
       </div>

      <input type="submit" value="Add Record" class="font-sans font-bold px-4 rounded cursor-pointer no-underline bg-green hover:bg-green-dark block w-full py-4 text-white items-center justify-center" />
    </form>

    <hr class="border border-grey-light my-6" />

    <ul class="list-reset mt-4">
      <li class="py-4" v-for="record in records" :key="record.id" :record="record">

        <div class="flex items-center justify-between flex-wrap">
          <div class="flex-1 flex justify-between flex-wrap pr-4">
            <p class="block font-mono font-semibold flex items-center">
              <svg class="fill-current text-indigo w-6 h-6 mr-2" viewBox="0 0 24 24" width="24" height="24"><title>record vinyl</title><path d="M23.938 10.773a11.915 11.915 0 0 0-2.333-5.944 12.118 12.118 0 0 0-1.12-1.314A11.962 11.962 0 0 0 12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12c0-.414-.021-.823-.062-1.227zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-5a1 1 0 1 0 0 2 1 1 0 0 0 0-2z" ></path></svg>
              {{ record.title }} &mdash; {{ record.year }}
            </p>
            <p class="block font-mono font-semibold">{{ getArtist(record) }}</p>
          </div>
          <button class="bg-transparent text-sm hover:bg-blue hover:text-white text-blue border border-blue no-underline font-bold py-2 px-4 mr-2 rounded"
          @click.prevent="editRecord(record)">Edit</button>

          <button class="bg-transparent text-sm hover:bg-red text-red hover:text-white no-underline font-bold py-2 px-4 rounded border border-red"
         @click.prevent="removeRecord(record)">Delete</button>
        </div>

        <div v-if="record == editedRecord">
          <form action="" @submit.prevent="updateRecord(record)">
            <div class="mb-6 p-4 bg-white rounded border border-grey-light mt-4">

              <div class="mb-6">
                <label class="label">Title</label>
                <input class="input" v-model="record.title" />
              </div>

              <div class="mb-6">
                <label class="label">Year</label>
                <input class="input" v-model="record.year" />
              </div>

              <div class="mb-6">
                <label class="label">Artist</label>
                <select id="artist" class="select" v-model="record.artist">
                  <option :value="artist.id" v-for="artist in artists" :key="artist.id">{{ artist.name }}</option>
                </select>
              </div>

              <input type="submit" value="Update" class="bg-transparent text-sm hover:bg-blue hover:text-white text-blue border border-blue no-underline font-bold py-2 px-4 mr-2 rounded">
            </div>
          </form>
        </div>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'Records',
  data () {
    return {
      artists: [],
      records: [],
      newRecord: [],
      error: '',
      editedRecord: ''
    }
  },
  created () {
    if (!localStorage.signedIn) {
      this.$router.replace('/')
    } else {
      this.$http.secured.get('/api/v1/records')
        .then(response => { this.records = response.data })
        .catch(error => this.setError(error, 'Something went wrong'))

      this.$http.secured.get('/api/v1/artists')
        .then(response => { this.artists = response.data })
        .catch(error => this.setError(error, 'Something went wrong'))
    }
  },
  methods: {
    setError (error, text) {
      this.error = (error.response && error.response.data && error.response.data.error) || text
    },
    getArtist (record) {
      const recordArtistValues = this.artists.filter(artist => artist.id === record.artist_id)
      let artist

      recordArtistValues.forEach(function (element) {
        artist = element.name
      })

      return artist
    },
    addRecord () {
      const value = this.newRecord
      if (!value) {
        return
      }
      this.$http.secured.post('/api/v1/records/', { record: { title: this.newRecord.title, year: this.newRecord.year, artist_id: this.newRecord.artist } })

        .then(response => {
          this.records.push(response.data)
          this.newRecord = ''
        })
        .catch(error => this.setError(error, 'Cannot create record'))
    },
    removeRecord (record) {
      this.$http.secured.delete(`/api/v1/records/${record.id}`)
        .then(response => {
          this.records.splice(this.records.indexOf(record), 1)
        })
        .catch(error => this.setError(error, 'Cannot delete record'))
    },
    editRecord (record) {
      this.editedRecord = record
    },
    updateRecord (record) {
      this.editedRecord = ''
      this.$http.secured.patch(`/api/v1/records/${record.id}`, { record: { title: record.title, year: record.year, artist_id: record.artist } })
        .catch(error => this.setError(error, 'Cannot update record'))
    }
  }
}
</script>
```

The `Records.vue` component is quite similar to the `Artists.vue` component in that the same basic CRUD operations are in full effect. I introduce the artist to record relation with a new `select` field which grabs data from our backend and saves it once a new record is saved. We loop through both `Record` and `Artist` data to get the necessary ids and fields back to save, edit, update and delete the fields correctly.

### Where to go next?

Our app is far from complete but it is functioning nicely. We have JWT-based authentication and a full CRUD based Vue app working on the frontend. Our backend is talking to the frontend the way we intended![🎉](https://s.w.org/images/core/emoji/11/72x72/1f389.png). I found one final bug in my Rails `artists_controller.rb` and `records_controller.rb` files that dealt with the `location:` property. Normally those would exist but I have removed them due to an odd namespacing issue I can’t quite figure out. Maybe you know the solution?

From here I invite you to extend the app and/or use it as a guide in your own projects. I learned a lot with this build. I have to admit, this was the hardest one I’ve taken on thus far. Hopefully, it’s enough to show you a new way to use Ruby on Rails with modern frontend frameworks and more.

### The Series so far

- [Let’s Build: With Ruby on Rails – Introduction](https://web-crunch.com/lets-build-with-ruby-on-rails-introduction/)
- [Let’s Build: With Ruby on Rails – Installation](https://web-crunch.com/lets-build-with-ruby-on-rails-installation/)
- [Let’s Build: With Ruby on Rails – Blog with Comments](https://web-crunch.com/lets-build-with-ruby-on-rails-blog-with-comments)
- [Let’s Build: With Ruby on Rails – A Twitter Clone](https://web-crunch.com/lets-build-with-ruby-on-rails-a-twitter-clone/)
- [Let’s Build: With Ruby on Rails – A Dribbble Clone](https://web-crunch.com/lets-build-dribbble-clone-with-ruby-on-rails/)
- [Let’s Build: With Ruby on Rails – Project Management App](https://web-crunch.com/lets-build-with-ruby-on-rails-project-management-app/)
- [Let’s Build: With Ruby on Rails – Discussion Forum](https://web-crunch.com/lets-build-with-ruby-on-rails-discussion-forum/)
- [Let’s Build: With Ruby on Rails – Deploying an App to Heroku](https://web-crunch.com/lets-build-with-ruby-on-rails-deploying-an-app-to-heroku/)
- [Let’s Build: With Ruby on Rails – eCommerce Music Shop](https://web-crunch.com/ruby-on-rails-ecommerce-music-shop/)
- [Let’s Build: With Ruby on Rails – Book Library App with Stripe Subscription Payments](https://web-crunch.com/lets-build-ruby-on-rails-book-library-stripe-subscription-payments/)
- [Let’s Build: With Ruby on Rails – Trade App With In-App Messaging](https://web-crunch.com/lets-build-with-ruby-on-rails-trade-app-in-app-messaging/)
- [Let’s Build: With Ruby on Rails – Multitenancy Workout Tracker App](https://web-crunch.com/lets-build-with-ruby-on-rails-multitenancy-workout-tracker-app/)
- [Let’s Build: With Ruby on Rails – Scheduling App with Payments](https://dev.to/justalever/lets-build-with-ruby-on-rails--event-scheduling-app-with-payments-2h7d-temp-slug-7516635)

### Shameless plug time

[![](https://i1.wp.com/web-crunch.com/wp-content/uploads/2019/01/YouTube-Promo3.jpg?resize=3840%2C2160&ssl=1)](http://hellorails.io)

I have a new course called [Hello Rails](https://hellorails.io). Hello Rails is a modern course designed to help you start using and understanding Ruby on Rails fast. If you’re a novice when it comes to Ruby or Ruby on Rails I invite you to check out the site. The course will be much like these builds but a super more in-depth version with more realistic goals and deliverables. [💌 Get notified](https://hellorails.io/)!

Follow [@hello\_rails](https://twitter.com/hello_rails) and [myself @justalever](https://twitter.com/justalever) on Twitter.

The post [Ruby on Rails API with Vue.js](https://web-crunch.com/ruby-on-rails-api-vue-js/) appeared first on [Web-Crunch](https://web-crunch.com).