DEV Community

maxiim3
maxiim3

Posted on

Create a Fullstack app with Vue and Go (And Tailwindcss v4)

This article is for new developers or people curious about Go, vue, or how to bulid a simple full stack application

About me

I am a front end developer exploring backend.
After trying PHP, Node.js, Rust... I felt in love with Golang for its simplicity, yet efficiency and performance.

The Stack

We gonna use Go and its light and easy library to build a server : Echo.
For the Front end we use Vue.js with Vite.
The client-side navigation is handled with Vue-router.
For the styling we gonna use the best tool out there : Tailwindcss. And "Cerise sur le gateau", we use the preview alpha version, v4, that should be released this summer.

Backend

Installation

Let's create a new directory and enter it

mkdir go-vue &&  cd $_
Enter fullscreen mode Exit fullscreen mode

Now we create the new go module.
We add the necessecary dependencies : echo/v4 for the server, echo/v4/middleware to handle the CORS

go mod init github.com/<username>/go-vue
go get github.com/labstack/echo/v4
go get github.com/labstack/echo/v4/middleware
Enter fullscreen mode Exit fullscreen mode

Then we can create a new go file touch main.go
Open VIM your favorite IDE.

First steps

Here are the basic : We create a new echo instance and start listening to port 8888

package main
import (
    "github.com/labstack/echo/v4"
)
func main() {
    e := echo.New()

    // Note that this must be the last statement of the main function
    e.Logger.Fatal(e.Start(":8888"))
}
Enter fullscreen mode Exit fullscreen mode

Serving static files

Right now our projects looks like this

.
├── go.mod
├── go.sum
└── main.go
Enter fullscreen mode Exit fullscreen mode

But we gonna create our Vue app inside ui/ which will contain the front-end code.
Using Vite we gonna build the Vue app into ui/dist and serve it from go.

So it's gonna look like this :

.
├── go.mod
├── go.sum
├── main.go
└── ui
    ├── dist
    │   ├── assets
    │   │   ├── index-D52G_CEl.css
    │   │   └── index-y7ffriUP.js
    │   └── index.html
    |-- ... vue files
Enter fullscreen mode Exit fullscreen mode

We need to tell Echo that we are using static files.
The documentation is great btw...

package main
import (
    "github.com/labstack/echo/v4"
)
func main() {
    e := echo.New()


    // We declare that route "/" will serve our local "/ui/dirst" project path
    e.Static("/", "ui/dist")
    // We serve index.html from the build directory as the root route
    e.File("/", "ui/dist/index.html")

    e.Logger.Fatal(e.Start(":8888"))
}
Enter fullscreen mode Exit fullscreen mode

Add an API

We gonna create a simple API in order to have some sort of communication between vue and go.
For simplicity we gonna use an object, but in a real app we would probably use a database.

We gonna have a Person object. We will send informations about it to Vue. And give the ability to Vue to update the Person's name.
So we need a Get and a Post request. We will also need to handle the CORS.

Create a Person

We use the Go struct and instanciate a person

type Person struct {
    Name  string `json:"name"`
    Age   uint8  `json:"age"`
    Email string `json:"email"`
}
//... inside main function
p := Person{
    Name:  "Nikola",
    Age:   37,
    Email: "nikola@tesla.genius",
}
Enter fullscreen mode Exit fullscreen mode

It will later be transleted in Typescript as :

type Person = {
    name: string
    age: number
    email: string
}
Enter fullscreen mode Exit fullscreen mode

GET a Person

We then send our person to the GET route /person

package main
import (
    "github.com/labstack/echo/v4"
)
func main() {
    e := echo.New()

    e.Static("/", "ui/dist")
    e.File("/", "ui/dist/index.html")

    // Create a Person
    p := Person{
        Name:  "Nikola",
        Age:   37,
        Email: "nikola@tesla.genius",
    }
    // Get Person on route "/person"
    e.GET("/person", func(c echo.Context) error {
        return c.JSON(202, p)
    })

    e.Logger.Fatal(e.Start(":8888"))
}

type Person struct {
    Name  string `json:"name"`
    Age   uint8  `json:"age"`
    Email string `json:"email"`
}
Enter fullscreen mode Exit fullscreen mode

You see why Go + Echo is awesome ? So easy !

Update a Person's name

We use the POST request to get the data from the Front end via POST method and update our local instance.

We setup the model Vue (Typescript) is gonna send to the POST route /person

type PostPersonName = {
    name: string
}
Enter fullscreen mode Exit fullscreen mode

Which translates in Go as

type PostPersonBody struct {
    Name string `json:"name"`
}
Enter fullscreen mode Exit fullscreen mode

Here is the full code with the POST request and the Middleware to handle the CORS

package main
import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
    "github.com/labstack/gommon/log"
)
func main() {
    e := echo.New()
    e.Static("/", "ui/dist")
    e.File("/", "ui/dist/index.html")

    // We gonna face CORS issue since we are passing data between different apps.
    e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
        AllowOrigins: []string{"http://localhost:8888", "http://localhost:5173"},
        AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete},
    }))

    p := Person{
        Name:  "Nikola",
        Age:   37,
        Email: "nikola@tesla.genius",
    }
    e.GET("/person", func(c echo.Context) error {
        return c.JSON(202, p)
    })

    // Update Person's name
    e.POST("/person", func(c echo.Context) error {
        // Get the request
        r := c.Request()
        // Read the body
        b, err := io.ReadAll(r.Body)
        if err != nil {
            log.Error("error in POST", err)
            return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid Body Request"})
        }
        n := PostPersonBody{
            Name: "default",
        }
        // equivalent of JSON.parse() in GO
        // By default Go passes arguments by value, meaning it creates a copy of the value, and a new pointer is created.
        // json.Unmarshall requires a reference (a pointer) to PostPersonBody and will update it internally.
        err = json.Unmarshal(b, &n)
        if err != nil {
            log.Error(err)
            return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid JSON"})
        }
        // Debug purpose
        fmt.Println(n.Name)
        // Update local instance (db...)
        p.Name = n.Name
        return c.JSON(http.StatusAccepted, n)
    })
    e.Logger.Fatal(e.Start(":8888"))
}
type Person struct {
    Name  string `json:"name"`
    Age   uint8  `json:"age"`
    Email string `json:"email"`
}
Enter fullscreen mode Exit fullscreen mode

At this point we can run go run ./main.go or go run ./ to lunch the server.

Note that any changes in the Go code requires to relunch the server.

Front end

Instalation

Let's install Vue.js using Vite.
For the router we will use vue-router.
For styling we will try the alpha version of the future Tailwind v4 release.

npm create vue@latest # we call the project `ui`
npm install vue-router@4
npm install tailwindcss@next @tailwindcss/vite@next
Enter fullscreen mode Exit fullscreen mode

Then add the tailwind plugin to the vite.config.js which should also contain the vue plugin

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(), 
    tailwindcss()
  ]
})
Enter fullscreen mode Exit fullscreen mode

Creating the app

What's cool with Vue is that it is just a simple html with a js script entry point containing all the vue app.

The javascript will be injected inside the root html component.

When the project will be built, it will just compact the js and css but the principle will stay the same. making it easy for us to serve the html file from Go.

Note that we use Vue 3 with the composition API.

Adding components

Create two simple components for our pages: HomePage and AboutPage

./ui
├── index.html
├── src
│   ├── App.vue
│   ├── components
│   │   ├── AboutPage.vue
│   │   └── HomePage.vue
│   ├── main.ts
│   ├── style.css
│   └── # ..
└── # ..
Enter fullscreen mode Exit fullscreen mode

About Page

<template>
    <h1>ABOUT</h1>
    <div>
        <p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Porro ipsam inventore corrupti. Ducimus, sunt corrupti?</p>
    </div>
</template>

<script lang="ts" setup></script>
Enter fullscreen mode Exit fullscreen mode

Home Page

The Home page will fetch Person from the Go API. And is able to update it (via POST route we created earlier.

<template>
    <main class="flex flex-col justify-center items-center gap-8 py-12 px-8">

        <h1 class="text-3xl text-blue-500">Welcome to Home page</h1>

        <!-- Display Person's name or text -->
        <p>Form API : {{ data?.name || "Click the Button 👇" }}</p>
        <!-- Click the button to (re)fetch the data from the API -->
        <button class="p-2 bg-lime-600 text-whiet font-bold rounded-md" @click="fetchData">{{ data ? "Refresh" : "Get data" }}</button>

        <div class="flex flex-col gap-4">
            <!-- v-model binds the input to the reference "name". It is both a Getter and a Setter -->
            <input class="p-2 rounded-md" placeholder="no data.." type="text" v-model="name">
            <!-- Fire the Post Request to update the name -->
            <button class="p-2 bg-lime-600 text-whiet font-bold rounded-md" @click="update">Update Name</button>
        </div>
        <p>Name model : {{ name }}</p>
    </main>
</template>
Enter fullscreen mode Exit fullscreen mode
<script lang="ts" setup>
import { ref } from 'vue';

type Person = {
    name: string
    age: number
    email: string
}

type PostPersonName = {
    name: string
}

const data = ref<Person | null>(null)

const name = defineModel<string>("fetching..")

async function fetchData() {
    const prom = await fetch("http://localhost:8888/person")
    const res: Awaited<Person> = await prom.json()
    data.value = res
    name.value = res.name
}

async function update() {
    const data: PostPersonName = {
        name: name.value!
    }

    const resp = await fetch("http://localhost:8888/person", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "*",
        },
        body: JSON.stringify(data)
    })

    console.log(resp)
}

</script>
Enter fullscreen mode Exit fullscreen mode

Root Component

We update the root component to use the RouterView Component in order to display the components based on the current url

<template>
  <nav class="w-full bg-slate-800 p-8">
    <ul>
      <li>
        <RouterLink to="/">Home</RouterLink>
      </li>
      <li>
        <RouterLink to="/about">About</RouterLink>
      </li>
    </ul>
  </nav>
  <RouterView />
</template>

<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router';

</script>
Enter fullscreen mode Exit fullscreen mode

Registering the routes in the root component

Now we can create our router, register the routes and update the vue app configuration.

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import HomePage from './components/HomePage.vue'
import AboutPage from './components/AboutPage.vue'
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'

// Routes registration
const routes: RouteRecordRaw[] = [
  { path: '/', component: HomePage },
  { path: '/about', component: AboutPage },
]

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

// Create app
createApp(App) // Root Component
  .use(router) // Use Router
  .mount('#app') // Html root element

Enter fullscreen mode Exit fullscreen mode

Run the app

You can check that everything works by launching the vite server with npm run dev

Compile the Full stack application

Build the Front end

To compile the application, we have to build the front end.

As we saw Go will source the path ui/dist: e.Static("/", "ui/dist")

Just run npm run build

Now we have this structure:

.
├── go.mod
├── go.sum
├── main.go
└── ui
    ├── README.md
    ├── dist
    │   ├── assets
    │   │   ├── index-D52G_CEl.css
    │   │   └── index-y7ffriUP.js
    │   ├── index.html
    │   └── vite.svg
    ├── index.html
    ├── package-lock.json
    ├── package.json
    ├── public
    │   └── vite.svg
    ├── src
    │   ├── App.vue
    │   ├── assets
    │   │   └── vue.svg
    │   ├── components
    │   │   ├── AboutPage.vue
    │   │   └── HomePage.vue
    │   ├── main.ts
    │   ├── style.css
    │   └── vite-env.d.ts
    ├── tsconfig.json
    ├── tsconfig.node.json
    └── vite.config.ts
Enter fullscreen mode Exit fullscreen mode

Build the backend

To compile Go into a single binary just go build ./main.go.

But this will ouput main as the binary name, and we want to rename it go-vue.

To do so run : go build -o go-vue ./main.go

Now you can run ./go-vue And the server should be running on port 8888

I hop you liked the article. Please share it and add your thoughts in the comment section.

Top comments (0)