Vue crud best practice structure with Laravel API

This article will walk you through implementing the CRUD functional features in Vue 2 with the proper structures. We will see how we can create a new one, retrieve an existing, and update an existing record in the database table. This will include using store (Vuex), creating services with Axios as a way to consume API, also we will create a data table reusable component for the list. This Vue CRUD article will use the CRUD API from this article CRUD API in Laravel: How to Create, Read, Update, and Delete. Data table component from this article Creating Dynamic Data Table with VueJs. Also, the text input component from this article VueJs - Create base text input component.

Note: There is little change in code from the reference article.

Setup product pages

Product list page to demonstrate the list.

In the ./src/pages/ProductList.vue

<template>
  <div>
    <h3 class="d-inline-block">Product List</h3>
    <button class="d-inline-block" @click="onCreate()">Create</button>
    <data-table
      :columns="columns"
      :entities="products"
      :pagination="pagination"
      @onPagination="onPagination"
    >
      <template v-slot:column_actions="{ entity }">
        <td>
          <button @click="onView(entity)">View</button>
          <button @click="onEdit(entity)">Edit</button>
          <button @click="onDelete(entity)">Delete</button>
        </td>
      </template>
    </data-table>
  </div>
</template>

<script>
import { mapState, mapActions } from "vuex";
import DataTable from "@/components/DataTable";
import ProductService from "@/services/ProductService";

export default {
  components: { DataTable },
  data() {
    return {
      columns: [
        {
          key: "id",
          label: "Id",
        },
        {
          key: "name",
          label: "Name",
        },
        {
          key: "price",
          label: "Price",
        },
      ],
    };
  },
  computed: {
    ...mapState("product", ["products", "pagination"]),
  },
  methods: {
    ...mapActions("product", ["getProducts"]),
    onCreate() {
      this.$router.push({ name: "AddProduct" });
    },
    onEdit(entity) {
      this.$router.push({ name: "EditProduct", params: { id: entity.id } });
    },
    onView(entity) {
      this.$router.push({ name: "ViewProduct", params: { id: entity.id } });
    },
    async onDelete(entity) {
      await ProductService.destroy(entity.id);
      this.getProducts();
    },
    onPagination(page) {
      this.getProducts({ current_page: page });
    },
  },
  created() {
    this.getProducts();
  },
};
</script>

<style scoped>
.d-inline-block {
  display: inline-block;
}
</style>

In the ./src/pages/ProductEntity.vue

ProductEntity page will use for three purposes add, edit and view.

<template>
  <div>
    <h3>Product management</h3>
    <base-text-input
      label="Name"
      v-model="form.name"
      :disabled="isView"
    ></base-text-input>
    <base-text-input
      label="Price"
      v-model="form.price"
      :disabled="isView"
    ></base-text-input>
    <button v-if="!isView" @click="submit()">Submit</button>
  </div>
</template>

<script>
import BaseTextInput from "@/components/BaseTextInput";
import ProductService from "@/services/ProductService";

export default {
  components: { BaseTextInput },
  data() {
    return {
      form: {
        name: "",
        price: "",
      },
    };
  },
  computed: {
    isEdit() {
      return this.$route.path.split("/").at(-1) === "edit";
    },
    isView() {
      return this.$route.path.split("/").at(-1) === "view";
    },
  },
  methods: {
    async submit() {
      if (this.isEdit) {
        await ProductService.update(this.$route.params.id, this.form);
      } else {
        await ProductService.store(this.form);
      }
      this.$router.push({ name: "ListProduct" });
    },
  },
  async created() {
    if (!this.isEdit && !this.isView) return;
    const { data } = await ProductService.show(this.$route.params.id);
    this.form = data;
  },
};
</script>

Setup component (That is used in the page above)

DataTable component

In the ./src/components/DataTable.vue

<template>
  <table>
    <tr>
      <slot
        v-for="(column, index) in columns"
        :name="`header_${column.key}`"
        :header="column"
      >
        <th :key="index">
          {{ column.label }}
        </th>
      </slot>
      <slot name="header_actions" v-if="isActions">
        <th>Action</th>
      </slot>
    </tr>
    <tr v-for="(entity, index) in entities" :key="`entity.${index}`">
      <slot
        v-for="(column, index) in columns"
        :name="`column_${column.key}`"
        :entity="entity"
      >
        <td :key="`column.${index}`">
          {{ entity[column.key] }}
        </td>
      </slot>
      <slot name="column_actions" :entity="entity" v-if="isActions">
        <td>
          <button @click="onEdit(entity)">Edit</button>
          <button @click="onDelete(entity)">Delete</button>
        </td>
      </slot>
    </tr>
    <div v-if="pagination">
      <button
        v-for="page in pagination.last_page"
        :key="page"
        @click="onPagination(page)"
      >
        {{ page }}
      </button>
    </div>
  </table>
</template>

<script>
export default {
  name: "DataTable",
  props: {
    columns: {
      required: true,
    },
    entities: {
      required: true,
    },
    pagination: {
      default: false,
    },
    isActions: {
      default: true,
      type: Boolean,
    },
  },
  methods: {
    onEdit(entity) {
      this.$emit("onEdit", entity);
    },
    onDelete(entity) {
      this.$emit("onDelete", entity);
    },
    onPagination(page) {
      this.$emit("onPagination", page);
    },
  },
};
</script>

<style scoped>
table {
  font-family: arial, sans-serif;
  border-collapse: collapse;
  width: 100%;
}

td,
th {
  border: 1px solid #dddddd;
  text-align: left;
  padding: 8px;
}

tr:nth-child(even) {
  background-color: #dddddd;
}
</style>

BaseTextInput component

In the ./src/components/BaseTextInput.vue

<template>
  <div>
    <label class="label">{{ label }}</label>
    <input
      type="text"
      v-model="content"
      :placeholder="placeholder"
      :disabled="disabled"
      :required="required"
    />
  </div>
</template>

<script>
export default {
  props: {
    required: {
      type: Boolean,
      default: false,
    },
    disabled: {
      type: Boolean,
      default: false,
    },
    placeholder: {
      type: String,
      default: "Input...",
    },
    label: {
      type: String,
      required: true,
    },
    value: {
      required: true,
    },
  },
  computed: {
    content: {
      get() {
        return this.value;
      },
      set(val) {
        this.$emit("input", val);
      },
    },
  },
};
</script>

<style scoped>
.label {
  display: block;
}
</style>

Setup vue-router

Requirement: vue-router package.

In the ./src/routes/index.js

import Vue from "vue";
import VueRouter from "vue-router";

Vue.use(VueRouter);

const router = new VueRouter({
  mode: "history",
  routes: [
    {
      path: "/product",
      name: "ListProduct",
      component: () => import("@/pages/ProductList"),
    },
    {
      path: "/product/add",
      name: "AddProduct",
      component: () => import("@/pages/ProductEntity"),
    },
    {
      path: "/product/:id/edit",
      name: "EditProduct",
      component: () => import("@/pages/ProductEntity"),
    },
    {
      path: "/product/:id/view",
      name: "ViewProduct",
      component: () => import("@/pages/ProductEntity"),
    },
  ],
});

export default router;

Now in the ./src/main.js 

Told Vuejs to use our created routes.

import Vue from "vue";
import App from "./App.vue";
import router from "./routes";

Vue.config.productionTip = false;

new Vue({
  router,
  render: (h) => h(App),
}).$mount("#app");

Setup services with Axios

Requirement: axios package.

Service is where we perform actions to the backend endpoints such as fetch, store, update and delete data from the database by API.

In the ./src/axios.js

import axios from "axios";

const http = axios.create({
  baseURL: "http://127.0.0.1:8000/api",
  headers: {
    Accept: "application/json",
  },
});

http.interceptors.response.use(
  (res) => {
    return Promise.resolve(res.data);
  },
  (err) => {
    return Promise.reject(err);
  }
);

export default http;

In the ./src/services/BaseService.js

import http from "@/axios";

export default class BaseService {
  static get METHOD_GET() {
    return "GET";
  }

  static get METHOD_POST() {
    return "POST";
  }

  static get METHOD_PUT() {
    return "PUT";
  }

  static get METHOD_DELETE() {
    return "DELETE";
  }

  constructor(prefix) {
    this.prefix = prefix;
  }

  index(data) {
    return this.performRequest(BaseService.METHOD_GET, "", data);
  }

  show(id) {
    return this.performRequest(BaseService.METHOD_GET, id);
  }

  store(data) {
    return this.performRequest(BaseService.METHOD_POST, "", data);
  }

  update(id, data) {
    return this.performRequest(BaseService.METHOD_PUT, id, data);
  }

  destroy(id) {
    return this.performRequest(BaseService.METHOD_DELETE, id);
  }

  performRequest(method, url, data = {}, headers = {}) {
    let endPoint = this.prefix + (url ? "/" + url : "");
    let options = {
      method,
      url: endPoint,
      data,
      headers,
    };

    options[
      method.toUpperCase() === BaseService.METHOD_GET ? "params" : "data"
    ] = data;
    return http(options);
  }
}

Now the product service, in the ./src/services/ProductService.js

import BaseService from "./BaseService";

class ProductService extends BaseService {
  constructor(prefix) {
    super(prefix);
  }
}

export default new ProductService("product");

Setup store with Vuex

Requirement: Vuex package.

Vuex (store) centralizes state management that made easy access to data from any components compare to the browser session there is little difference just the state is reset on the refresh page and the session only reset when the session expired means that the browser tab is closed.

The main state, getters, mutations, and actions are the main store module and the product module we create in the ./modules/product.js and import back into the Vuex modules.

In the ./src/stores/index.js

import Vue from "vue";
import Vuex from "vuex";

import state from "./state";
import getters from "./getters";
import mutations from "./mutations";
import actions from "./actions";

import product from "./modules/product";

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    product,
  },
  state,
  getters,
  mutations,
  actions,
});

In the ./src/stores/state.js

export default {

}

In the ./src/stores/getters.js

export default {

}

In the ./src/stores/mutations.js

export default {

}

In the ./src/stores/actions.js

export default {

}

Now the main part, Our product store to demonstrate CRUD.

In the ./src/stores/modules/product.js

import ProductService from "@/services/ProductService";

export default {
  namespaced: true,
  state: () => ({
    products: [],
    pagination: {},
  }),
  getters: {},
  mutations: {
    setProducts(state, val) {
      state.products = val;
    },
    setPagination(state, val) {
      state.pagination = val;
    },
  },
  actions: {
    async getProducts({ commit }, params = {}) {
      const { data, pagination } = await ProductService.index(params);
      commit("setProducts", data);
      commit("setPagination", pagination);
    },
  },
};

Now in the ./src/main.js 

Told Vuejs to use our created store.

import Vue from "vue";
import App from "./App.vue";
import router from "./routes";
import store from "./stores";

Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: (h) => h(App),
}).$mount("#app");

Conclusion

This article not just shows how to do crud in Vue 2 but will show you how to do a complete CRUD with proper structures with this, you will have:

  • Services for consuming APIs
  • Store for state management
  • Setup vue-router for routing
  • Create reusable component