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 $_
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
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"))
}
Serving static files
Right now our projects looks like this
.
├── 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 :
.
├── 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...
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
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 :
type Person = {
name: string
age: number
email: string
}
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"`
}
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
}
Which translates in Go as
type PostPersonBody struct {
Name string `json:"name"`
}
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"`
}
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
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()
]
})
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
│ └── # ..
└── # ..
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>
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>
<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
<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
.
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:
.
├── 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.
Top comments (0)