Vue CRUD App with a Go API

Building a Vue CRUD App with a Go API

Mastering CRUD operations is essential for creating dynamic web apps. In this article, we’ll walk you through the process of building a CRUD app using Vue on the client side and a Go API on the server side, demonstrating the effectiveness of this tech stack.

Prerequisites

  • Node.js
  • Go 1.21
  • MySQL

Setup Vue project

npm create vite@4.4.0 view -- --template vue
cd view
npm install vue-router@4 axios

Vue project structure

├─ index.html
├─ public
│  └─ css
│     └─ style.css
└─ src
   ├─ App.vue
   ├─ components
   │  └─ product
   │     ├─ Create.vue
   │     ├─ Delete.vue
   │     ├─ Detail.vue
   │     ├─ Edit.vue
   │     ├─ Index.vue
   │     └─ Service.js
   ├─ http.js
   ├─ main.js
   └─ router.js

Vue project files

main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')

This main.js file is the entry point for a Vue.js application. It sets up and mounts the app with routing by importing the root component and router, creating the app instance, and configuring it with the router before mounting it to the #app element.

App.vue

<template>
  <router-view />
</template>

<script>
export default {
  name: 'App'
}
</script>

This App.vue file defines the root component of a Vue.js application. It uses a <router-view /> in the template to display routed components based on the current route.

router.js

import { createWebHistory, createRouter } from 'vue-router'

const routes = [
  {
    path: '/',
    redirect: '/product'
  },
  {
    path: '/product',
    name: 'product',
    component: () => import('./components/product/Index.vue')
  },
  {
    path: '/product/create',
    component: () => import('./components/product/Create.vue')
  },
  {
    path: '/product/:id/',
    component: () => import('./components/product/Detail.vue')
  },
  {
    path: '/product/edit/:id/',
    component: () => import('./components/product/Edit.vue')
  },
  {
    path: '/product/delete/:id/',
    component: () => import('./components/product/Delete.vue')
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes,
})

export default router

This router.js file configures routing for a Vue.js application. It sets up routes for various product-related views, including listing, creating, editing, and deleting products, as well as a default redirect to the product list. The router uses `createWebHistory` for HTML5 history mode and exports the configured router instance.

http.js

import axios from 'axios'

let http = axios.create({
  baseURL: 'http://localhost:5122/api',
  headers: {
    'Content-type': 'application/json'
  }
})

export default http

The http.js file configures and exports an Axios instance with a centralized base URL, which is a standard practice for managing API endpoints and default headers set to application/json.

Create.vue

<template>
  <div class="container">
    <div class="row">
      <div class="col">
        <form method="post" @submit.prevent="create()">
          <div class="row">
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_name">Name</label>
              <input id="product_name" name="Name" class="form-control" v-model="product.name" maxlength="50" />
            </div>
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_price">Price</label>
              <input id="product_price" name="Price" class="form-control" v-model="product.price" type="number" />
            </div>
            <div class="col-12">
              <router-link class="btn btn-secondary" to="/product">Cancel</router-link>
              <button class="btn btn-primary">Submit</button>
            </div>
          </div>
        </form>
      </div>
    </div>
  </div>
</template>
<script>
import Service from './Service'

export default {
  name: 'ProductCreate',
  data() {
    return {
      product: {}
    }
  },
  methods: {
    create() {
      Service.create(this.product).then(() => {
        this.$router.push('/product')
      }).catch((e) => {
        alert(e.response.data)
      })
    }
  }
}
</script>

This Create.vue component provides a form for adding a new product with fields for name and price. On submission, it calls a create method to save the product and redirects to the product list upon success. It also includes a cancel button to navigate back to the list and handles errors with an alert.

Delete.vue

<template>
  <div class="container">
    <div class="row">
      <div class="col">
        <form method="post" @submit.prevent="this.delete()">
          <div class="row">
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_id">Id</label>
              <input readonly id="product_id" name="Id" class="form-control" :value="product.id" type="number" required />
            </div>
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_name">Name</label>
              <input readonly id="product_name" name="Name" class="form-control" :value="product.name" maxlength="50" />
            </div>
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_price">Price</label>
              <input readonly id="product_price" name="Price" class="form-control" :value="product.price" type="number" />
            </div>
            <div class="col-12">
              <router-link class="btn btn-secondary" to="/product">Cancel</router-link>
              <button class="btn btn-danger">Delete</button>
            </div>
          </div>
        </form>
      </div>
    </div>
  </div>
</template>
<script>
import Service from './Service'

export default {
  name: 'ProductDelete',
  data() {
    return {
      product: {}
    }
  },
  mounted() {
    this.get()
  },
  methods: {
    get() {
      return Service.delete(this.$route.params.id).then(response => {
        this.product = response.data
      })
    },
    delete() {
      Service.delete(this.$route.params.id, this.product).then(() => {
        this.$router.push('/product')
      }).catch((e) => {
        alert(e.response.data)
      })
    }
  }
}
</script>

The Delete.vue component provides a form for deleting a product, with read-only fields for the product's. The component fetches the product details when mounted. The form calls a delete method to remove the product and redirects to the product list upon success.

Detail.vue

<template>
  <div class="container">
    <div class="row">
      <div class="col">
        <form method="post">
          <div class="row">
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_id">Id</label>
              <input readonly id="product_id" name="Id" class="form-control" :value="product.id" type="number" required />
            </div>
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_name">Name</label>
              <input readonly id="product_name" name="Name" class="form-control" :value="product.name" maxlength="50" />
            </div>
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_price">Price</label>
              <input readonly id="product_price" name="Price" class="form-control" :value="product.price" type="number" />
            </div>
            <div class="col-12">
              <router-link class="btn btn-secondary" to="/product">Back</router-link>
              <router-link class="btn btn-primary" :to="`/product/edit/${product.id}`">Edit</router-link>
            </div>
          </div>
        </form>
      </div>
    </div>
  </div>
</template>
<script>
import Service from './Service'

export default {
  name: 'ProductDetail',
  data() {
    return {
      product: {}
    }
  },
  mounted() {
    this.get()
  },
  methods: {
    get() {
      return Service.get(this.$route.params.id).then(response => {
        this.product = response.data
      })
    }
  }
}
</script>

The Detail.vue component displays detailed information about a product. It features read-only fields for the product's. The component fetches product details when mounted. It includes a "Back" button to navigate to the product list and an "Edit" button to navigate to the product's edit page.

Edit.vue

<template>
  <div class="container">
    <div class="row">
      <div class="col">
        <form method="post" @submit.prevent="edit()">
          <div class="row">
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_id">Id</label>
              <input readonly id="product_id" name="Id" class="form-control" v-model="product.id" type="number" required />
            </div>
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_name">Name</label>
              <input id="product_name" name="Name" class="form-control" v-model="product.name" maxlength="50" />
            </div>
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_price">Price</label>
              <input id="product_price" name="Price" class="form-control" v-model="product.price" type="number" />
            </div>
            <div class="col-12">
              <router-link class="btn btn-secondary" to="/product">Cancel</router-link>
              <button class="btn btn-primary">Submit</button>
            </div>
          </div>
        </form>
      </div>
    </div>
  </div>
</template>
<script>
import Service from './Service'

export default {
  name: 'ProductEdit',
  data() {
    return {
      product: {}
    }
  },
  mounted() {
    this.get()
  },
  methods: {
    get() {
      return Service.edit(this.$route.params.id).then(response => {
        this.product = response.data
      })
    },
    edit() {
      Service.edit(this.$route.params.id, this.product).then(() => {
        this.$router.push('/product')
      }).catch((e) => {
        alert(e.response.data)
      })
    }
  }
}
</script>

The Edit.vue component provides a form for editing an existing product. It includes fields for the product's. The component fetches the product details when mounted and updates the product on form submission. It also features a "Cancel" button to navigate back to the product list and a "Submit" button to save the changes.

Index.vue

<template>
  <div class="container">
    <div class="row">
      <div class="col">
        <table class="table table-striped table-hover">
          <thead>
            <tr>
              <th>Id</th>
              <th>Name</th>
              <th>Price</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="product in products" :key="product">
              <td class="text-center">{{product.id}}</td>
              <td>{{product.name}}</td>
              <td class="text-center">{{product.price}}</td>
              <td class="text-center">
                <router-link class="btn btn-secondary" :to="`/product/${product.id}`" title="View"><i class="fa fa-eye"></i></router-link>
                <router-link class="btn btn-primary" :to="`/product/edit/${product.id}`" title="Edit"><i class="fa fa-pencil"></i></router-link>
                <router-link class="btn btn-danger" :to="`/product/delete/${product.id}`" title="Delete"><i class="fa fa-times"></i></router-link>
              </td>
            </tr>
          </tbody>
        </table>
        <router-link class="btn btn-primary" to="/product/create">Create</router-link>
      </div>
    </div>
  </div>
</template>
<script>
import Service from './Service'

export default {
  name: 'ProductIndex',
  data() {
    return {
      products: []
    }
  },
  mounted() {
    this.get()
  },
  methods: {
    get() {
      Service.get().then(response => {
        this.products = response.data
      }).catch(e => {
        alert(e.response.data)
      })
    }
  }
}
</script>

The Index.vue component displays a table of products with columns for ID, name, and price. It fetches the list of products when mounted and populates the table. Each product row includes action buttons for viewing, editing, and deleting the product. There is also a "Create" button for adding new products.

Service.js

import http from '../../http'

export default {
  get(id) {
    if (id) {
      return http.get(`/products/${id}`)
    }
    else {
      return http.get('/products' + location.search)
    }
  },
  create(data) {
    if (data) {
      return http.post('/products', data)
    }
    else {
      return http.get('/products/create')
    }
  },
  edit(id, data) {
    if (data) {
      return http.put(`/products/${id}`, data)
    }
    else {
      return http.get(`/products/${id}`)
    }
  },
  delete(id, data) {
    if (data) {
      return http.delete(`/products/${id}`)
    }
    else {
      return http.get(`/products/${id}`)
    }
  }
}

The Service.js file defines API methods for handling product operations. It uses an http instance for making requests:

  • get(id) Retrieves a single product by ID or all products if no ID is provided.
  • create(data) Creates a new product with the provided data or fetches the creation form if no data is provided.
  • edit(id, data) Updates a product by ID with the provided data or fetches the product details if no data is provided.
  • delete(id, data) Deletes a product by ID or fetches the product details if no data is provided.

style.css

.container {
    margin-top: 2em;
}

.btn {
    margin-right: 0.25em;
}

The CSS adjusts the layout by adding space above the container and spacing out buttons horizontally.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" rel="stylesheet">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
  <link href="/css/style.css" rel="stylesheet">
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>

The HTML serves as the main entry point for a Vue application. It includes Bootstrap for styling and Font Awesome for icons. The application will render within a div with the ID app.

Setup Go API project

go mod init app
go get github.com/gin-gonic/gin
go get github.com/gin-contrib/cors
go get gorm.io/gorm
go get gorm.io/driver/mysql
go get github.com/joho/godotenv

Create a testing database named "example" and execute the database.sql file to import the table and data.

Go API Project structure

├─ .env
├─ main.go
├─ config
│  └─ db.go
├─ controllers
│  └─ product_controller.go
├─ models
│  └─ product.go
└─ router
   └─ router.go

Go API Project files

.env

DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=example
DB_USER=root
DB_PASSWORD=

This file holds the configuration details for connecting to the database.

db.go

package config

import (
	"fmt"
	"os"

	"github.com/joho/godotenv"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/schema"
)

var DB *gorm.DB

func SetupDatabase() {
	godotenv.Load()
	connection := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true", os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_DATABASE"))
	db, _ := gorm.Open(mysql.Open(connection), &gorm.Config{NamingStrategy: schema.NamingStrategy{SingularTable: true}})
	DB = db
}

This db.go file configures the database connection with GORM. The SetupDatabase function loads environment variables, constructs a MySQL connection string, and initializes a GORM instance, which is stored in the global DB variable.

router.go

package router

import (
	"app/controllers"
	
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
)

func SetupRouter() {
	productController := controllers.ProductController{}
	corsConfig := cors.DefaultConfig()
    corsConfig.AllowAllOrigins = true
	router := gin.Default()
	router.Use(cors.New(corsConfig))
	router.Group("/api").
		GET("/products", productController.Index).
		POST("/products", productController.Create).
		GET("/products/:id", productController.Get).
		PUT("/products/:id", productController.Update).
		DELETE("/products/:id", productController.Delete)
	router.Run()
}

This router.go file sets up routing for a Go application using the Gin framework. The SetupRouter function initializes a Gin router with CORS middleware to allow all origins. It defines routes for handling product-related operations under the /api/products path, each mapped to a method in the ProductController. Finally, it starts the Gin server.

product.go

package models

type Product struct {
	Id    int `gorm:"primaryKey;autoIncrement"`
	Name  string
	Price float64
}

This product.go file defines a Product model for use with GORM. It specifies a Product struct with three fields: Id (an auto-incrementing primary key), Name, Price.

product_controller.go

package controllers

import (
	"app/config"
	"app/models"
	"net/http"
	"strconv"

	"github.com/gin-gonic/gin"
)

type ProductController struct {
}

func (con *ProductController) Index(c *gin.Context) {
	var products []models.Product
	config.DB.Find(&products)
	c.JSON(http.StatusOK, products)
}

func (con *ProductController) Get(c *gin.Context) {
	var product models.Product
	config.DB.First(&product, c.Params.ByName("id"))
	c.JSON(http.StatusOK, product)
}

func (con *ProductController) Create(c *gin.Context) {
	var product models.Product
	c.BindJSON(&product)
	if err := config.DB.Create(&product).Error; err != nil {
		c.AbortWithError(http.StatusBadRequest, err)
		return
	}
	c.JSON(http.StatusOK, product)
}

func (con *ProductController) Update(c *gin.Context) {
	var product models.Product
	c.BindJSON(&product)
	product.Id, _ = strconv.Atoi(c.Params.ByName("id"))
	if err := config.DB.Updates(&product).Error; err != nil {
		c.AbortWithError(http.StatusBadRequest, err)
		return
	}
	c.JSON(http.StatusOK, product)
}

func (con *ProductController) Delete(c *gin.Context) {
	var product models.Product
	if err := config.DB.Delete(&product, c.Params.ByName("id")).Error; err != nil {
		c.AbortWithError(http.StatusBadRequest, err)
		return
	}
	c.Status(http.StatusOK)
}

The product_controller.go file defines a ProductController struct with methods to handle CRUD operations for products in a Go application using the Gin framework.

  • Index Retrieves and returns a list of all products.
  • Get Fetches and returns a single product by its ID.
  • Create Creates a new product from the request body.
  • Update Updates an existing product with the data from the request body.
  • Delete Deletes a product by its ID.

main.go

package main

import (
	"app/config"
	"app/router"
)

func main() {
	config.SetupDatabase()
	router.SetupRouter()
}

This main.go file is the entry point for the Go application. It imports configuration and routing packages, then calls config.SetupDatabase()to initialize the database connection and router.SetupRouter() to set up the application’s routes.

Run projects

Run Vue project

npm run dev

Run Go API project

go run main.go

Open the web browser and goto http://localhost:5173
You will find this product list page.

list page

Testing

Click the "View" button to see the product details page.

details page

Click the "Edit" button to modify the product and update its details.

edit page

Click the "Submit" button to save the updated product details.

updated data

Click the "Create" button to add a new product and input its details.

create page

Click the "Submit" button to save the new product.

created product

Click the "Delete" button to remove the previously created product.

delete page

Click the "Delete" button to confirm the removal of this product.

deleted product

Conclusion

In conclusion, we have learned how to create a basic Vue project using Single-File Components (SFC) for building views and defining application routing. By setting up the API with the Gin framework as the backend and utilizing GORM for database operations, we've developed a dynamic front-end that integrates effectively with a powerful backend, establishing a solid foundation for modern, full-stack web applications.

Source code: https://github.com/stackpuz/Example-CRUD-Vue-3-Go

Create a Vue CRUD App in Minutes: https://stackpuz.com

Related post