How to Create a Simple CRUD App With Rails and Vue

This tutorial was inspired by James Hibbard’s post but instead of using React, I will use Vue. This is not meant to compare React and Vue nor to conclude which one is better though I find Vue more approachable. This is just my second time to use Vue so please don’t look at me as an expert. This is also a learning experience for me and I hope you learn something, too :)

While most of the content are based on James' post, I decided to treat Rails and Vue as two separate applications. The Rails application is API-only that serves JSON, i.e. you will not see any mention of Vue included in the codebase. On the other hand, the Vue application is also a pure web client and does everything via API calls. I will also use Vuex to centrally managed the API calls and the state used by the Vue components.

In this post, I decided not to do a typical tutorial where you go about building the application from start to finish. Instead, I will just highlight the major code changes.


The tutorial was developed using the following:

The code is available at

Quick Guide

# Get the code
git checkout
cd rails-vue-tutorial

# Run Rails app
# To view JSON localhost:3000/events
cd rails-api
bundle install
rails db:setup
rails db:seed
rails s

# Run Vue app
# To view app localhost:8080/events
cd vue-ui
npm install
npm run serve

# Or using foreman to run the 2 apps
foreman start -f

Start here:



The project is composed of 2 applications - a Rails application that returns events in JSON and a Vue application that handles UI interaction. The applications are in their respective directories.


API Endpoints

GET     /events      # list of events
POST    /events      # create a new event
PUT     /events/:id  # update an event
DELETE  /events/:id  # delete an event

Each event has the following structure:

  "id": 1,
  "event_type": "Symposium",
  "event_date": "2018-05-01",
  "title": "A Social-Neuroscience Perspective on Empathy",
  "speaker": "Albert von Bezold, Jules Cotard, Marian Diamond",
  "host": "Alcmaeon of Croton",
  "published": true,
  "created_at": "2019-04-13T22:41:55.867Z",
  "updated_at": "2019-04-13T22:41:55.867Z"

Rails Components


The EventsController is a typical Rails controller that returns a JSON using Jbuilder. However, I would love to share a pattern that I often use with my Rails controller.

class EventsController < ApplicationController
  def index

  def create

    # more code follows

  def update

    # more code follows

  def destroy

    # more code follows


  def event_scope

  def load_events
    @events = event_scope.order(event_date: :DESC)

  def load_event
    @event ||= event_scope.find(params[:id])

  def build_event
    @event ||=
    @event.attributes = event_params

  def event_params
    @event_params = params[:event]
    @event_params ? @event_params.permit(permitted_params) : {}

  def permitted_params

Did you notice it?

I like separating the actions into disctinct methods even though it often just results to one-liners. However, this clean separation makes the controller easier to understand and evolve. For example, event_scope may eventually evolve to scope with current_user.

Our Rail application is boring but I love it that way. It is all Ruby and there is no Javascript framework to worry about

Since we have built an API on top of Rails, we need to tell our Rails application that requests would come from a diferent source. Otherwise, our Vue application will just receive errors every time it calls the API. If you’re planning on serving your Vue application other than from localhost, update the origins parameter and restart your Rails application.

# config/initializers/cors.rb

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

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

Here is good introduction about CORS in the context of Rails if you like to dig deeper.

Vue Components

Components are building blocks of a Vue-based application. You might represent the page header as a component, or the sidebar, the list of events, or even just the event itself. A single page can also be a component composed of several components, and so on.

Components under src/views, by convention, maps to URLs and represent the top-level page. The mapping is listed in src/router.js.




  1. User action is captured by a Vue component.
  2. Vue component calls a Vuex action that calls an API (via axios library).
  3. Rails handles the API and returns a JSON response.
  4. Vuex mutates a state based on the response.
  5. The Vue component updates the page based on the state changes.

In this sequence, the Vue component has no idea about the API calls - it simply calls a Vuex action. The Vuex action has no idea about about the Vue component - it only cares about the API request/response and the state to be modified. The Vue component reacts to changes to the state.

Vuex and State Management

When building an application, you often how to deal with these three things:

  • state - the source of truth
  • view (or views) - mapping the state for the components
  • actions - ways the state could change

In our sample application, the state is the list of events, the view is the component the displays each event, and the actions are loading the events via the API, adding new event, etc.

The beauty of Vuex (and other state management libraries like Redux) is it enables a clean separation between state, views, and actions and at the same, they work together in a harmonious way. It was an aha! moment when I finally I understood what it does.

Listing Events

Let’s see how the Vue pieces fit together in the context of listing events.

First, initialize list of events.

// src/main.js#9
const store = initStore({
  [states.EVENTS]: []

Visiting the URL /events loads the EventsView component. The mapping is specified using the Vue Router.

// src/router.js#11
  path: "/events",
  name: "events",
  component: EventsView

After the component is loaded, invoke the action GET_EVENTS

// src/views/EventsView.vue#63
created() {

The GET_EVENTS action is just another method that calls the API to fetch the list of events. The state is then mutated with the response data.

// src/store/actions/index.js#6
[actions.GET_EVENTS]({ state, commit }) {
  return api
    .then(response => {

// src/api.js#12
getEvents() {
  return axios.get("/events");

The list events from the API call is then saved. Or, using the right technical term, the state is mutated.

// src/store/mutations/index.js#8
[mutations.SET_EVENTS](state, events) {
  state[states.EVENTS] = events;

The component EventsView listens to any changes to the list of events. It then passes the list to another component EventList.

// src/views/EventsView.vue#12
  <EventList :events="events"/>

The EventList component takes care of actually displaying the events.

// src/components/EventList.vue
    <div v-for="event in events" :key="">

Adding and Updating Events

Of course, our application will be useless if it can only list events. So, let’s include a way to add events to our application.

First, add an event handler when a link (or button) is clicked.

// src/views/EventsView.vue#7
<a @click="newEvent">New Event</a>

// src/views/EventsView.vue#90
newEvent() {
  this.$router.push({ name: "newEvent" });

The newEvent() method pushes the path /events/new via the Vue router which we used to determine when to show the EventForm component.

// src/views/EventsView.vue#19
<div v-if="showEvent">
  <Event :event="event"/>
<div v-else-if="showEventForm">
  <EventForm :event="event"/>

The conditionals are just methods that checks the current path.

// src/views/EventsView.vue#73
showEvent() {
  return (this.$ == "event") && (this.event !== null);
showEventForm() {
  return (this.$ == "newEvent") && (this.event !== null);

The EventForm component receives the event selected from the EventsList component. In Vue, passing an object from a parent component to a child component is done via props.

// src/components/EventForm.vue#99
props: {
  event: {
    type: Object,
    required: true

The EventForm component handles the saving of events whether it is part of adding a new event or updating an existing one sequence. The component also follows the state-view-actions pattern. First, we create a handler when the form is submitted.

// src/components/EventForm.vue#6
<form v-on:submit.prevent="saveEvent()" className="eventForm">

// src/components/EventForm.vue#109
saveEvent() {
    .then(() => {
      this.$emit("eventSelected", this.aEvent);

The SAVE_EVENT action is just a method that accepts an event object and takes care of calling the API.

// src/store/actions/index.js#13
[actions.SAVE_EVENT]({ state, commit }, event) {
  if ( {
    return api
      .then(response => {
  } else {
    return api
      .then(response => {

After receiving the response from the API, the list of events is updated.

// src/store/mutations/index.js#8
[mutations.SET_EVENT](state, event) {
  const i = state[states.EVENTS].findIndex(e => ==;

  if (i >= 0) {
    // DOM will not be updated if you modify via index.
    state[states.EVENTS].splice(i, 1, event);
  } else {

Since the EventList component is watching the events list object, it automatically updates the list when an event is added or updated.

Deleting an event

There is nothing special with deleting an event - it just follows the state-view-action pattern like the other scenarios.

// src/views/EventsView.vue#98
deleteEvent(event) {
    .then(() => {
      this.$router.push({ name: "events" });

// src/store/actions/index.js#28
[actions.DELETE_EVENT]({ state, commit }, event) {
  if ( {
    return api
      .then(response => {
        commit(mutations.DELETE_EVENT, event);

// src/store/mutations/index.js#18
[mutations.DELETE_EVENT](state, event) {
  const i = state[states.EVENTS].findIndex(e => ==;

  if (i >= 0) {
    state[states.EVENTS].splice(i, 1);


I was curious how to test Vue applications so I added a simple unit test using Jest. If you like to use a different framework, the vue-cli create command will allow you to choose one.

To help in our testing, we use a factory method to prepare the component to be tested.

// tests/unit/Event.spec.js
import { shallowMount } from "@vue/test-utils";
import Event from "@/components/Event";

const factory = (values = {}) => {
  return shallowMount(Event, {
    propsData: {
      event: { ...values }

shallowMount loads a component and stubs the child components which is perfect for unit testing. It also allows you to set the data or props attributes of the component, for example:

describe("Event", () => {
  it("renders the event details", () => {
    const wrapper = factory({
      title: "event title",
      event_type: "symposium"

    expect(wrapper.find(".test-event-type").text()).toEqual("Type: symposium");
    expect(wrapper.find(".test-title").text()).toEqual("Title: event title");

To run the tests:

npm run test:unit

Just follow the Vue testing guide if you want to dive deeper into Vue testing.


At this point, I hope you have a good idea how the different parts of Vue works together. Like I said in the beginning, I’m not an expert in Vue nor have I extensive experience on other Javascript frameworks like React. Though personally, I find Vue more approachable and I will definitely use this in my personal projects.

If you are new to Vue (like I am), I highly recommend you read their list of beginner gotchas. I guarantee you in will save you lots of frustrations :)

If you are interested in comparing Vue with other frameworks, the article by Yogev Ahuvia is a good read.