Create a Fullstack app with Vue and Go (And Tailwindcss v4)
Tags: go, vue, web3, vite
Published: 2024-05-29 17:29:22

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

```bash
mkdir go-vue &&  cd $_
```

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

```bash
go mod init github.com/<username>/go-vue
go get github.com/labstack/echo/v4
go get github.com/labstack/echo/v4/middleware
```

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`

```go
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"))
}
```

## Serving static files

Right now our projects looks like this

```text
.
├── go.mod
├── go.sum
└── main.go
```

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 :

```text
.
├── go.mod
├── go.sum
├── main.go
└── ui
    ├── dist
    │   ├── assets
    │   │   ├── index-D52G_CEl.css
    │   │   └── index-y7ffriUP.js
    │   └── index.html
    |-- ... vue files
```

We need to tell Echo that we are using static files.
[The documentation is great btw...](https://echo.labstack.com/docs/static-files)

```go
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"))
}
```

### 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

```go
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",
}
```

It will later be transleted in Typescript as :

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

#### GET a Person

We then send our person to the *GET route* `/person`

```go
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"`
}
```

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`

```ts
type PostPersonName = {
    name: string
}
```

Which translates in Go as

```go
type PostPersonBody struct {
    Name string `json:"name"`
}
```

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

```go
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"`
}
```

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](https://router.vuejs.org/).
For styling we will try the alpha version of the future [Tailwind v4 release](https://tailwindcss.com/blog/tailwindcss-v4-alpha#using-vite).

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

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

```javascript
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()
  ]
})
```

### 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](https://vuejs.org/guide/extras/composition-api-faq.html#what-is-composition-api).

### Adding components

Create two simple components for our pages: `HomePage` and `AboutPage`

```text
./ui
├── index.html
├── src
│   ├── App.vue
│   ├── components
│   │   ├── AboutPage.vue
│   │   └── HomePage.vue
│   ├── main.ts
│   ├── style.css
│   └── # ..
└── # ..
```

#### About Page

```html
<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>
```

#### Home Page

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

```html
<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>
```

```html
<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>
```

### Root Component

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

```html
<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>
```

## Registering the routes in the root component

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

```typescript
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

```

### 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:

```text
.
├── 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
```

### 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.