Building A Full-Stack Application With Vue, Vuex and Rails

Vue is a popular front-end that is especially useful for Rails developers since it was designed to be incrementally adoptable. That means you can use Vue for parts of your UI without having to rebuild everything from scratch. In this article, John Emmanual will introduce us to Vue, show us how to set it up in Rails, and walk us through a simple project.

  • author photo By John Emmanuel
  • #ruby
  • Aug 16, 2021

Building full-stack applications with scalability in mind can be intimidating, especially when building with the latest version of Vue and Vuex, which have full typescript support. This article will teach its readers everything they need to know about building scalable full-stack applications from state management with Vuex 4.0 to handle API requests and database interactions by exploring a CRUD application that manages prescription of treatments to unhealthy livestock. The backend will be built with Rails, exposing basic CRUD APIs for integration by the frontend.

Most companies adopted API-based development architecture since it gives the frontend development team more flexibility to choose a suitable frontend tool for development. Although this may not be the best option, it has been a reliable option, and many companies have adopted this development architecture since it adds more flexibility to the team.

Prerequisites

Before we get started, make sure you have the following to keep up with this tutorial:

  • Rails V6.x
  • Node.js V10.x
  • Prior working knowledge of Ruby on Rails
  • Prior working knowledge of TypeScript
  • Prior working knowledge of Vue.js

What we will build

Image of what our app will look like

In this tutorial, we will build a full-stack CRUD application that manages the prescription of treatments for unhealthy livestock. Users will be able to create, update, and delete prescriptions for livestock. The CRUD APIs will be built using Rails, exposing the CRUD APIs for integration by the frontend, which will be built with Vue 3. Each of these stacks will run on different ports as independent applications.

Basic Architecture of the Full-stack App

Our full-stack application is made up of both client and server applications running independently, with each component of the client application interacting with the CRUD APIs issued by the server application through Vuex for proper management of the application state. The backend application stores all prescription data in a Sqlite 3 database while exposing the CRUD APIs to the frontend application.

Backend Service Setup

In a folder of your preference, create a Rails app by running the following command:

rails new vet_clinic_api --api

This will tell Rails to create this project as an API, thereby removing all the frontend dependencies(view files).

Database Configuration

We will use Sqlite 3, which is the default database for Rails applications.

rails g scaffold prescriptions vet_prescription:text prescribed_by:text disease:text livestock:text completed:boolean

By running the above command, Rails will scaffold an initial structure for our migration, test, model, controller, route:

rails db:migrate

This command will add our tables to the database.

Seeding the Database

Let’s seed our database with some prescription data. Add the code snippet below to db/migrate/seed.rb

//db/migrate/seed.rb

Prescription.destroy_all
Prescription.create!([{
    vet_prescription:"Achyranthes aspera",
    prescribed_by:"Dr Chucks",
    disease:"Rabbies",
    livestock:"goat",
    completed:false
},
{
    vet_prescription:"Achyranthes aspera",
    prescribed_by:"Dr Rex",
    disease:"Rabbies",
    livestock:"Dog",
    completed:false
},
{
    vet_prescription:"ethnovet",
    prescribed_by:"Dr Chucks",
    disease:"Pox",
    livestock:"Sheep",
    completed:false
}])
p "Created #{Prescription.count} prescriptions"

This file will store the initial data for seeding the database so that when the app starts up, we’ll have some existing prescription data.

Run the following command to execute the code in db/migrate/seed.rb, which seeds the database with some predefined prescription data:

rails db:seed

With these few commands, we created a functional CRUD API with Rails. How easy was it? (smiles)

CORS Configuration

Since we’ll be exposing our CRUD APIs to the frontend, and both the frontend and backend servers will be running on a different PORT, we need to set up a CORS configuration on the Rails backend to grant access to data sharing between the frontend and backend.

Locate the Gemfile in the project root and uncomment the following line of code:

# gem 'rack-cors'

Add the following code to config/environments/initializers/cors.rb:

//config/environments/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

The above snippet allows the Rails CRUD API to be accessible from frontend apps running on any PORT.

Run the following command to start the Rails server:

rails s

Navigate to localhost:3000/prescriptions to get a JSON response for all prescriptions.

Consider using Rails if you are working with clients who change their projects’ workflow frequently. With Rails, it’s easy to implement features with a few commands and lines of code. This is my personal opinion, though.

Introducing Vue

Vue is a progressive framework for building user interfaces. Vue introduces concepts, such as Virtual DOM, render functions, and server-side rendering capabilities, to provide significant performance optimization for web applications.

Vue 3 comes with lots of new features and changes for developers. These features are designed to improve the overall stability of the framework, as well as its speed and maintainability.

We’ll make use of the composition API, one of the most anticipated features of Vue 3. It is a new way to create Vue components, with a more organized and efficient approach to writing code, and full TypeScript type-checking support.

Introducing Vuex

Vuex is a state management library created by the Vue team, and it is based on the same flux architecture as Redux. It is specifically designed for Vue and allows better organization of your store. Vuex will become crucial if your Vue application state gets more complex as it grows. The latest stable release of Vuex, v4.0.0, supports the Composition API introduced in Vue 3, as well as a more robust inference for TypeScript.

Setting up the Frontend Application

The frontend will be setup with Vue 3 and typeScript, while Vuex will be used for application state management.

Let’s start by using the Vue-CLI tool to create a Vue 3 app with typescript support.

Install the Vue-CLI tool globally with the following command:

npm install --global @vue/cli

Create a new Vue 3 app with typescript and Vuex support with the command below:

vue create vet_clinic_frontend

Choose the manually select feature option and hit the space key to select the following options:

  • Choose Vue version
  • Babel
  • TypeScript
  • Linter / Formatter

Next, choose Vue 3.x(Preview) as the version for the project.

  • Enter yes to use the class-style component syntax.
  • Enter yes to use Babel alongside TypeScript.
  • Select any linter of your choice.

Once the Vue 3 app is generated successfully, we will have a Vue 3 project setup with Vuex (V4.x) and full typescript support.

We’ll add type safety to the application using typescript. To start the development server, run the command below in your terminal and head over to http://localhost:8080 to preview your project in the browser.

We will use the Bulma CSS framework for styling our application. Run the following command to install Bulma CSS:

npm install bulma

To import Bulma CSS, add the following code to App.vue:

//App.vue
<style lang="scss">
@import "~bulma/css/bulma.css";
</style>

Setting up the Application Store

The application store will be setup with Vuex. To make changes to the store, actions will be dispatched from the component, which triggers the mutations, thereby updating the store.

Take the following steps to set up the application store:

  1. Create the state objects.
  2. Set up the mutations that will occur in our application.
  3. Create the actions that will commit to these subsequent mutations.
  4. Create getters for components to directly compute state data.

State

A state is a store object that holds application-level data that needs to be accessed by all components.

Create a state.ts file in the store directory with the following code snippet:

//src/store/state.ts
export type Prescription = {
  id: number;
  vet_prescription: string;
  prescribed_by: string;
  disease: string;
  livestock: string;
  completed: boolean;
  editing: boolean;
};
export type Data = {
  vet_prescription: string;
  prescribed_by: string;
  disease: string;
  livestock: string;
};
export type State = {
  loading: boolean;
  prescriptions: Prescription[];
  data: Data | null;
  showCreateModal: boolean;
  showEditModal: boolean;
  showPrescriptionModal: boolean;
  editModalPrescriptionId: number | undefined;
  showPrescriptionId: number | undefined;
};
export const state: State = {
  loading: false,
  prescriptions: [],
  data: null,
  showCreateModal: false,
  showEditModal: false,
  showPrescriptionModal: false,
  editModalPrescriptionId: undefined,
  showPrescriptionId: undefined,
};

Here, we add some type safety to Prescription and Data. We also export types because they will be used in the definitions of getters, mutations, and actions. Finally, we cast the State type to the state.

Mutations

Mutations are methods that, when triggered, modify the store. They receive the state as the first argument and payload as the second, eventually modifying the application state with the payload. To create mutations, the Vuex docs recommend using constants for mutation types.

Create a mutations.ts file in the store directory with the following code snippet:

//src/store/mutations.ts
import { MutationTree } from "vuex";
import { State, Prescription, Data } from "./state";

export enum MutationType {
  CreatePrescription = "CREATE_PRESCRIPTION",
  SetPrescriptions = "SET_PRESCRIPTIONS",
  CompletePrescription = "COMPLETE_PRESCRIPTION",
  RemovePrescription = "REMOVE_PRESCRIPTION",
  EditPrescription = "EDIT_PRESCRIPTION",
  UpdatePrescription = `UPDATE_PRESCRIPTION`,

  SetLoading = "SET_LOADING",
  SetCreateModal = "SET_CREATE_MODAL",
  SetEditModal = "SET_EDIT_MODAL",
  SetPrescriptionModal = "SET_PRESCRIPTION_MODAL",
}

The above snippet uses the enum symbol to hold all the possible names of mutations in our app.

Next, we will declare a contract (type) for each MutationType, as follows:

//src/store/mutation.ts
export type Mutations = {
  [MutationType.CreatePrescription](state: State, prescription: Data): void;

  [MutationType.SetPrescriptions](state: State, prescription: Prescription[]): void;

  [MutationType.CompletePrescription](state: State, prescription: Partial<Prescription> & { id: number }): void;

  [MutationType.RemovePrescription](state: State, prescription: Partial<Prescription> & { id: number }): void;

  [MutationType.EditPrescription](state: State, prescription: Partial<Prescription> & { id: number }): void;

  [MutationType.UpdatePrescription](state: State, prescription: Partial<Prescription> & { id: number }): void;

  [MutationType.SetLoading](state: State, value: boolean): void;

  [MutationType.SetCreateModal](state: State, value: boolean): void;

  [MutationType.SetEditModal](state: State, value: { showModal: boolean; prescriptionId: number | undefined }): void;

  [MutationType.SetPrescriptionModal](state: State, value: { showModal: boolean; prescriptionId: number | undefined }): void;
};

Then, we will implement the contracts declared for each MutationType, as follows:

//src/store/mutation.ts
export const mutations: MutationTree<State> & Mutations = {
  [MutationType.CreatePrescription](state, prescription) {
    state.data == prescription;
  },
  [MutationType.SetPrescriptions](state, prescriptions) {
    state.prescriptions = prescriptions;
  },
  [MutationType.CompletePrescription](state, newPrescription) {
    const prescription = state.prescriptions.findIndex((prescription) => prescription.id === newPrescription.id);
    if (prescription === -1) return;
    state.prescriptions[prescription] = { ...state.prescriptions[prescription], ...newPrescription };
  },
  [MutationType.RemovePrescription](state, Prescription) {
    const prescription = state.prescriptions.findIndex((prescription) => prescription.id === Prescription.id);
    if (prescription === -1) return;
    //If prescription exist in the state, remove it
    state.prescriptions.splice(prescription, 1);
  },
  [MutationType.EditPrescription](state, Prescription) {
    const prescription = state.prescriptions.findIndex((prescription) => prescription.id === Prescription.id);
    if (prescription === -1) return;
    //If prescription exist in the state, toggle the editing property
    state.prescriptions[prescription] = { ...state.prescriptions[prescription], editing: !state.prescriptions[prescription].editing };
    console.log("prescription", state.prescriptions[prescription]);
  },
  [MutationType.UpdatePrescription](state, Prescription) {
    state.prescriptions = state.prescriptions.map((prescription) => {
      if (prescription.id === Prescription.id) {
        return { ...prescription, ...Prescription };
      }
      return prescription;
    });
  },

  [MutationType.SetLoading](state, value) {
    state.loading = value;
  },
  [MutationType.SetCreateModal](state, value) {
    state.showCreateModal = value;
  },
  [MutationType.SetEditModal](state, value) {
    state.showEditModal = value.showModal;
    state.editModalPrescriptionId = value.prescriptionId;
  },
  [MutationType.SetPrescriptionModal](state, { showModal, prescriptionId }) {
    state.showPrescriptionModal = showModal;
    state.showPrescriptionId = prescriptionId;
  },
};

MutationTree is a generic type shipped with the Vuex package. We used it in the above snippet to declare a type of mutation tree. The mutation tree and mutations ensure that the contract is implemented correctly; otherwise, Typescript will trigger an error.

Actions

Actions are methods that trigger mutations. When handling asynchronous tasks, such as making request to an API, actions are used before calling the corresponding mutations with the API response as payload. We will get a clear implementation for this scenario as we create our actions.

Before creating actions, we will install Axios to handle all our Http request to the Rails server, as follows:

npm install axios --save

Create an actions.ts file in the store directory with the following code snippet:

//src/store/actions.ts
import { ActionContext, ActionTree } from "vuex";
import { Mutations, MutationType } from "./mutations";
import { State, Prescription, Data } from "./state";
import axios from "axios";
const apiUrl = "http://localhost:3000/prescriptions";
export enum ActionTypes {
  GetPrescriptions = "GET_PRESCRIPTIONS",
  SetCreateModal = "SET_CREATE_MODAL",
  SetEditModal = "SET_EDIT_MODAL",
  RemovePrescription = "REMOVE_PRESCRIPTION",
  CreatePrescription = "CREATE_PRESCRIPTION",
  UpdatePrescription = "UPDATE_PRESCRIPTION",
}

Similarly, the above snippet uses the enum symbol to hold all the possible names of actions in our app.

Next, we will declare a contract (type) for each ActionType, as follows:

//src/store/actions.ts
type ActionAugments = Omit<ActionContext<State, State>, "commit"> & {
  commit<K extends keyof Mutations>(key: K, payload: Parameters<Mutations[K]>[1]): ReturnType<Mutations[K]>;
};

export type Actions = {
  [ActionTypes.GetPrescriptions](context: ActionAugments): void;
  [ActionTypes.SetCreateModal](context: ActionAugments): void;
  [ActionTypes.SetEditModal](context: ActionAugments): void;
  [ActionTypes.RemovePrescription](context: ActionAugments, Prescription: { id: number }): void;
  [ActionTypes.CreatePrescription](context: ActionAugments, data: Data): void;
  [ActionTypes.UpdatePrescription](context: ActionAugments, prescription: Prescription): void;
};

The ActionAugments type restricts all commits only to their declared mutations and also to check the payload type.

Next, we will implement the contract (types) declared for each ActionType. Add the code below to the action.ts file:

//src/store/actions.ts
export const actions: ActionTree<State, State> & Actions = {
  async [ActionTypes.GetPrescriptions]({ commit }) {
    commit(MutationType.SetLoading, true);

    const response = await axios.get(apiUrl);

    commit(MutationType.SetLoading, false);
    commit(MutationType.SetPrescriptions, response.data);
  },

  async [ActionTypes.SetCreateModal]({ commit }) {
    commit(MutationType.SetCreateModal, true);
  },

  async [ActionTypes.SetEditModal]({ commit }) {
    commit(MutationType.SetEditModal, { showModal: true, prescriptionId: 1 });
  },

  //Optimistic update
  async [ActionTypes.RemovePrescription]({ commit }, Prescription) {
    if (Prescription != undefined) {
      commit(MutationType.RemovePrescription, Prescription);
    }

    const response = await axios.delete(`${apiUrl}/${Prescription.id}`);
  },

  async [ActionTypes.CreatePrescription]({ commit, dispatch }, Prescription) {
    const response = await axios.post(apiUrl, Prescription);
    dispatch(ActionTypes.GetPrescriptions);
  },

  async [ActionTypes.UpdatePrescription]({ commit, dispatch }, Prescription) {
    if (Prescription != undefined) {
      commit(MutationType.UpdatePrescription, Prescription);
      const response = await axios.patch(`${apiUrl}/${Prescription.id}`, Prescription);
      dispatch(ActionTypes.GetPrescriptions);
    }
  },
};

Here, we created an actions variable that stores all the implemented actions. Similarly, the ActionTree<State> & Actions ensures that the contract (type Actions) is implemented correctly; otherwise, Typescript will trigger an error.

We also set up our asynchronous call to the Rails API endpoint in the GetPrescriptions action and triggered the SetPrescriptions mutation type with the response data as the payload. We also set up SetCreateModal, SetEditModal, CreatePrescription, UpdatePrescription, and RemovePrescription actions.

Getters

Getters are methods that receive the state as its first parameter and return computed information from the store state.

Create a getters.ts file in the store directory with the following code snippet:

//src/store/getters.ts
import { GetterTree } from "vuex";
import { State, Prescription } from "./state";
export type Getters = {
  completedPrescriptionCount(state: State): number;
  totalPrescriptionCount(state: State): number;
  getPrescriptionById(state: State): (id: number) => Prescription | undefined;
};
export const getters: GetterTree<State, State> & Getters = {
  completedPrescriptionCount(state) {
    return state.prescriptions.filter((prescription) => prescription.completed).length;
  },
  totalPrescriptionCount(state) {
    return state.prescriptions.length;
  },
  getPrescriptionById: (state) => (id: number) => {
    return state.prescriptions.find((prescription) => prescription.id === id);
  },
};

The above code snippet defines the following getters:

  • completedPrescriptionCount – A function that gets the total number of completed prescriptions in our state.
  • totalPrescriptionCount – A function that gets the total number of prescriptions in our state.
  • getPrescriptionById – A function that gets a prescription by its ID.

We have also added some type safety to Getters.

Store

Let’s wire our state, mutations, actions, and getters to the global Vuex store. We’ll update store/index.ts, as follows:

//src/store/index.ts
import { createStore, Store as VuexStore, CommitOptions, DispatchOptions, createLogger } from "vuex";
import { State, state } from "./state";
import { Mutations, mutations } from "./mutations";
import { Actions, actions } from "./actions";
import { Getters, getters } from "./getters";
export const store = createStore<State>({
  plugins: process.env.NODE_ENV === "development" ? [createLogger()] : [],
  state,
  mutations,
  actions,
  getters,
});
export function useStore() {
  return store as Store;
}
export type Store = Omit<VuexStore<State>, "getters" | "commit" | "dispatch"> & {
  commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(key: K, payload: P, options?: CommitOptions): ReturnType<Mutations[K]>;
} & {
  dispatch<K extends keyof Actions>(key: K, payload?: Parameters<Actions[K]>[1], options?: DispatchOptions): ReturnType<Actions[K]>;
} & {
  getters: {
    [K in keyof Getters]: ReturnType<Getters[K]>;
  };
};

The state, mutations, actions, and getters are required to create a store by passing an object containing them into the createStore method. During development, the createLogger plugin logs the state (the previous state and next state) and mutations to the console. To have the store accessible in all our application’s components; we need to inject it into the entire application. Fortunately, the Vue-CLI tool has already imported the entire store and passed it within the application’s Vue instance.

Integrating the Vuex Store into the Component

In this tutorial, the Vue 3 composition API will be used to create all of components in our frontend application.

App Component

Our frontend should render a list of prescription data as soon as it launches. We’ll dispatch the GetPrescription action within the component’s mounted() lifecycle hook. To access the store within a component, the useStore hook, which returns our store, will be executed.

//src/App.vue
<script lang="ts">
import { computed, defineComponent, onMounted } from "vue";
import PrescriptionList from "./components/PrescriptionList.vue";
import { useStore } from "./store";
import { ActionTypes } from "./store/actions";
export default defineComponent({
  components: { PrescriptionList },
  setup() {
    const store = useStore();
    const loading = computed(() => store.state.loading);
    onMounted(() => store.dispatch(ActionTypes.GetPrescriptions));
    const completedCount = computed(() => store.getters.completedPrescriptionCount);
    const totalCount = computed(() => store.getters.totalPrescriptionCount);
    return { loading, completedCount, totalCount };
  },
});
</script>
<template>
  <nav class="navbar" role="navigation" aria-label="main navigation">
    <div class="navbar-brand">
      <a class="navbar-item" href="https://bulma.io">
        <img src="https://bulma.io/images/bulma-logo.png" width="112" height="28" />
      </a>
    </div>
    <div id="navbarBasicExample" class="navbar-menu">
      <div class="navbar-start">
        <a class="navbar-item"> Home </a>
        <a class="navbar-item"> About </a>
      </div>
    </div>
  </nav>
  <div class="container mx-auto mt-4">
    <h1 class="is-size-3 has-text-centered p-2 has-text-weight-bold is-success">Vet clinic Frontend</h1>
    <h3 class="has-text-centered p-2">Manage records of treated livestock in your farm</h3>
    <div v-if="loading">
      <h3 class="has-text-centered mt-4">Loading...</h3>
    </div>
    <div v-else>
      <p class="has-text-centered mt-2">{{ completedCount }} of {{ totalCount }} treated.</p>
      <PrescriptionList />
    </div>
  </div>
</template>
<style>
@import "~bulma/css/bulma.css";
</style>

Here, we’ve created three computed properties:

  • completedCount, which calls the completedPrescriptionCount getter method to retrieve the total number of completed treatments.
  • totalCount, which calls the totalPrescriptionCount getter method to retrieve the total number of prescriptions.
  • loading, which gets the loading property of the state.

With the Vue 3 composition API, the methods and properties required by the template must be returned to be accessible in the template. Notice how we’ve returned loading, completedCount, and totalCount.

PrescriptionList Component

This component will be responsible for retrieving the list of prescriptions from the backend and passing the prescription data to it child component.

Create PrescriptionList.vue inside the components folder with the following code:

//src/components/PrescriptionList.vue
<template>
  <table class="table is-hoverable is-striped">
    <thead>
      <tr>
        <th><abbr title="Position">Prescription Id</abbr></th>
        <th>Treated</th>
        <th>Prescription</th>
        <th><abbr title="Won">Prescribed By</abbr></th>
        <th><abbr title="Drawn">Disease</abbr></th>
        <th><abbr title="Drawn">Livestock</abbr></th>
        <th><abbr title="Lost">Actions</abbr></th>
      </tr>
    </thead>
    <tbody v-if="prescriptions">
      <tr v-for="prescription in prescriptions" :key="prescription.id">
        <PrescriptionListItem v-bind="prescription" />
      </tr>
    </tbody>
    <tfoot>
      <CreateModal v-show="showCreateModal"></CreateModal>
      <button class="button  is-success" @click="setModal">Create Prescription</button>
    </tfoot>
  </table>
  <EditModal v-if="showEditModal" :id="editModalPrescriptionId"></EditModal>
  <Prescription v-if="showPrescriptionModal" :id="showPrescriptionId"></Prescription>
</template>
<script>
import CreateModal from "./CreateModal";
import EditModal from "./EditModal";
import Prescription from "./Prescription";
import PrescriptionListItem from "./PrescriptionListItem";
import { defineComponent, computed } from "vue";
import { useStore } from "@/store";
import { MutationType } from "@/store/mutations";
export default defineComponent({
  name: "Table",
  components: {
    CreateModal,
    PrescriptionListItem,
    Prescription,
    EditModal,
  },
  setup() {
    const store = useStore();
    const setModal = () => {
      store.commit(MutationType.SetCreateModal, true);
    };
    const showCreateModal = computed(() => store.state.showCreateModal);
    const showEditModal = computed(() => store.state.showEditModal);
    const editModalPrescriptionId = computed(() => store.state.editModalPrescriptionId);
    const showPrescriptionModal = computed(() => store.state.showPrescriptionModal);
    const showPrescriptionId = computed(() => store.state.showPrescriptionId);
    const prescriptions = computed(() => store.state.prescriptions);
    return { showCreateModal, setModal, prescriptions, showEditModal, showPrescriptionModal, editModalPrescriptionId, showPrescriptionId };
  },
});
</script>
<style scoped>
table {
  width: 100%;
}
.fa {
  font-size: 1.2rem;
  margin-left: 15px;
}
.fa:hover {
  font-size: 1.4rem;
}
</style>

The setModal method invokes the mutation that sets showCreateModal in the state to true, resulting in launching the modal for creating prescriptions.

We’ve created the following computed properties:

  • showCreateModal, which gets the showCreateModal property of the state.
  • showEditModal, which gets the showEditModal property of the state.
  • showPrescriptionModal, which gets the showPrescriptionModal property of the state.
  • prescription, which gets the list of prescription from the state.
  • showPrescriptionId, which gets the showPrescriptiond property of the state.
  • editPrescriptionId, which gets the editPrescriptionId property of the state.

Prescription Component

This component will receive the prescription ID as a prop from the PrescriptionList component. ID props will be used to fetch a prescription with the corresponding ID via the getPrescriptionById getters method and render the prescription properties in the browser.

Create Prescription.vue inside the components folder with the following code:

//src/components/Prescription.vue
<template>
  <div class="modal is-active">
    <div class="modal-background"></div>
    <div class="modal-content">
      <h1>VIEW PRESCRIPTION</h1>
      <div class="card">
        <div class="card-content">
          <div class="media">
            <div class="media-content">
              <p class="title is-4">Livestock: {{ prescription.livestock }}</p>
              <p class="subtitle is-6"><b>Prescribed by:</b> {{ prescription.prescribed_by }}</p>
              <p class="subtitle is-6"><b>Disease:</b> {{ prescription.disease }}</p>
            </div>
          </div>
          <div class="content">
            <p class="subtitle is-6">Prescription: {{ prescription.vet_prescription }}</p>
          </div>
        </div>
      </div>
    </div>
    <button class="modal-close is-large" @click="closeModal" aria-label="close"></button>
  </div>
</template>
<script lang="ts">
import { computed } from "vue";
import { useStore } from "@/store";
import { MutationType } from "@/store/mutations";
export default {
  name: "PrescriptionModal",
  props: {
    id: { type: Number, required: true },
  },
  setup(props: any) {
    const store = useStore();
    const prescription = computed(() => store.getters.getPrescriptionById(Number(props.id)));
    const closeModal = () => {
      store.commit(MutationType.SetPrescriptionModal, {
        showModal: false,
        prescriptionId: undefined,
      });
    };
    return { closeModal, prescription };
  },
};
</script>
<style scoped>
h1 {
  color: #ffffff;
  text-align: center;
  font-size: 2rem;
  margin-bottom: 3rem;
}
</style>

The closeModal method commits the SetPrescriptionModal mutation, which sets the showModal property in the state to false, while the prescription computed property calls the getPrescriptionById getter method to retrieve a prescription by its Id.

CreateModal Component

This component is responsible for creating prescriptions.

Create CreateModal.vue inside the components folder with the following code:

//src/components/CreateModal.vue
<template>
  <div class="modal is-active">
    <div class="modal-background"></div>
    <div class="modal-content">
      <form @submit.prevent="createPrescription">
        <div class="field">
          <label class="label">Prescribed By</label>
          <div class="control">
            <input v-model="prescribedBy" class="input" type="text" placeholder="Enter prescriber's name" />
          </div>
        </div>
        <div class="field">
          <label class="label">Prescription</label>
          <div class="control">
            <textarea v-model="prescription" class="textarea" placeholder="Enter prescription"></textarea>
          </div>
        </div>
        <div class="field">
          <label class="label">Disease</label>
          <div class="control">
            <input v-model="disease" class="input" type="text" placeholder="Enter name of disease" />
          </div>
        </div>
        <div class="field">
          <label class="label">Livestock</label>
          <div class="control">
            <input v-model="livestock" class="input" type="text" placeholder="Enter livestock" />
          </div>
        </div>
        <div class="field is-grouped">
          <div class="control">
            <button type="submit" class="button is-link">Submit</button>
          </div>
          <div class="control" @click="closeModal">
            <button class="button is-link is-light">Cancel</button>
          </div>
        </div>
      </form>
    </div>
    <button class="modal-close is-large" @click="closeModal" aria-label="close"></button>
  </div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from "vue";
import { useStore } from "@/store";
import { Data } from "@/store/state";
import { MutationType } from "@/store/mutations";
import { ActionTypes } from "@/store/actions";
export default {
  name: "CreateModal",
  setup() {
    const state = reactive({
      prescription: "",
      prescribedBy: "",
      disease: "",
      livestock: "",
    });
    const store = useStore();
    const createPrescription = () => {
      if (state.prescription === "" || state.prescribedBy === "" || state.disease === "" || state.livestock === "") return;
      const prescription: Data = {
        vet_prescription: state.prescription,
        prescribed_by: state.prescribedBy,
        disease: state.disease,
        livestock: state.livestock,
      };
      store.dispatch(ActionTypes.CreatePrescription, prescription);
      state.prescription = "";
      state.prescribedBy = "";
      state.disease = "";
      state.livestock = "";
    };
    const closeModal = () => {
      store.commit(MutationType.SetCreateModal, false);
    };
    return { closeModal, ...toRefs(state), createPrescription };
  },
};
</script>

The createPrescription method dispatches an action that makes a post request to the server, thereby creating a new prescription, while the closeModal method commits the SetPrescriptionModal mutation (which sets the showModal property in the state to false).

Working with forms and input element requires two-way data binding, and as such, we used Vue 3’s reactive method to store values used in the input fields.

Note: When using reactive, we need to use toRefs to convert the reactive object to a plain object, where each property on the resulting object is a ref pointing to the corresponding property in the original object.

EditModal Component

This component is responsible for updating prescriptions. Its logic is similar to the CreatePrescription component we discussed in the previous section.

Create EditModal.vue inside the components folder with the following code:

//src/components/EditModal.vue
<template>
  <div class="modal is-active">
    <div class="modal-background"></div>
    <div class="modal-content">
      <form @submit.prevent="updatePrescription">
        <h1>Edit Modal</h1>
        <div class="field">
          <label class="label">Prescribed By</label>
          <div class="control">
            <input v-model="prescribedBy" class="input" type="text" placeholder="Enter prescriber's name" />
          </div>
        </div>
        <div class="field">
          <label class="label">Prescription</label>
          <div class="control">
            <textarea v-model="prescription" class="textarea" placeholder="Enter Prescription"></textarea>
          </div>
        </div>
        <div class="field">
          <label class="label">Disease</label>
          <div class="control">
            <input v-model="disease" class="input" type="text" placeholder="Enter name of disease" />
          </div>
        </div>
        <div class="field">
          <label class="label">Livestock</label>
          <div class="control">
            <input v-model="livestock" class="input" type="text" placeholder="Enter livestock" />
          </div>
        </div>
        <div class="field is-grouped">
          <div class="control">
            <button type="submit" class="button is-link">Submit</button>
          </div>
          <div class="control" @click="closeModal">
            <button class="button is-link is-light">Cancel</button>
          </div>
        </div>
      </form>
    </div>
    <button class="modal-close is-large" @click="closeModal" aria-label="close"></button>
  </div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, computed, onMounted } from "vue";
import { useStore } from "@/store";
import { Prescription } from "@/store/state";
import { MutationType } from "@/store/mutations";
import { ActionTypes } from "@/store/actions";
export default {
  name: "EditModal",
  props: {
    id: { type: Number, required: true },
  },
  setup(props: any) {
    const state = reactive({
      prescription: "",
      prescribedBy: "",
      disease: "",
      livestock: "",
    });
    const store = useStore();
    const setFields = () => {
      const prescription = store.getters.getPrescriptionById(Number(props.id));
      if (prescription) {
        console.log("prescription si kolo", prescription);
        state.prescription = prescription.vet_prescription;
        state.prescribedBy = prescription.prescribed_by;
        state.disease = prescription.disease;
        state.livestock = prescription.livestock;
      }
    };
    onMounted(() => {
      setFields();
    });
    const updatePrescription = () => {
      if (state.prescription === "" || state.prescribedBy === "" || state.disease === "" || state.livestock === "") return;
      const prescription: Prescription = {
        id: props.id,
        vet_prescription: state.prescription,
        prescribed_by: state.prescribedBy,
        disease: state.disease,
        livestock: state.livestock,
        completed: false,
        editing: false,
      };
      store.dispatch(ActionTypes.UpdatePrescription, prescription);
      state.prescription = "";
      state.prescribedBy = "";
      state.disease = "";
      state.livestock = "";
    };
    const closeModal = () => {
      store.commit(MutationType.SetEditModal, { showModal: false, prescriptionId: undefined });
    };
    return { closeModal, ...toRefs(state), updatePrescription };
  },
};
</script>
<style scoped>
label {
  color: #ffffff;
}
h1 {
  color: #ffffff;
  text-align: center;
  font-size: 2rem;
  margin-bottom: 3rem;
}
</style>

The createPrescription method dispatches an action that makes a put request to the server, thereby updating an existing prescription by its ID, while the closeModal method commits the SetPrescriptionModal mutation that sets the showModal property in the state to false. Calling the setFields method on the onMounted lifecycle hook triggers the getPrescriptionById getters method to fetch a prescription from the store by its ID and then updates the properties in the reactive object with the fetched prescription properties as soon as the component is mounted on the DOM.

Launching the Final Project

In the root directory of your Rails API server, run the following command to start the server:

rails server

Now, you can run the frontend application with the following command:

npm run serve

Finally, your application should be as follows:

Image of what our app should look like

Conclusion

We have built a CRUD API server with Rails and our frontend application on Vue 3 composition API and Vuex, all running on different servers while integrating both stacks to build a fullstack web application. I hope you have learned a great deal from this tutorial. Please reach out in the comment section below if you have any questions or suggestions. Here is the GitHub repo for the full-stack application built in this tutorial.

What to do next:
  1. Try Honeybadger for FREE
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Start free trial
    Easy 5-minute setup — No credit card required
  2. Get the Honeybadger newsletter
    Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo

    John Emmanuel

    John is a fullstack software developer, mentor, and writer. He's am an open source enthusiast. In my spare time, I enjoy watching sci-fi movies and cheering for Arsenal FC.

    Stop wasting time manually checking logs for errors!

    Try the only application health monitoring tool that allows you to track application errors, uptime, and cron jobs in one simple platform.

    • Know when critical errors occur, and which customers are affected.
    • Respond instantly when your systems go down.
    • Improve the health of your systems over time.
    • Fix problems before your customers can report them!

    As developers ourselves, we hated wasting time tracking down errors—so we built the system we always wanted.

    Honeybadger tracks everything you need and nothing you don't, creating one simple solution to keep your application running and error free so you can do what you do best—release new code. Try it free and see for yourself.

    Start free trial
    Simple 5-minute setup — No credit card required

    Learn more

    "We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release."
    — Michael Smith, Cofounder & CTO of YvesBlue

    Honeybadger is trusted by top companies like:

    “Everyone is in love with Honeybadger ... the UI is spot on.”
    Molly Struve, Sr. Site Reliability Engineer, Netflix
    Start free trial
    Are you using Sentry, Rollbar, Bugsnag, or Airbrake for your monitoring? Honeybadger includes error tracking with a whole suite of amazing monitoring tools — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
    Start free trial
    Stop digging through chat logs to find the bug-fix someone mentioned last month. Honeybadger's built-in issue tracker keeps discussion central to each error, so that if it pops up again you'll be able to pick up right where you left off.
    Start free trial
    “Wow — Customers are blown away that I email them so quickly after an error.”
    Chris Patton, Founder of Punchpass.com
    Start free trial