
Série Vue.js + Go com Echo v5: Parte 1: ambiente e primeira API · Parte 2: frontend Vue com Vite · Parte 3: API REST de tarefas · Parte 4: login com sessão e cookie · Parte 5: embed, binário único e Docker
Código completo: todos os arquivos da série estão no gist vue-go-echo-v5, com um README que mostra em qual pasta cada um fica.
Na parte 1 subimos a primeira rota da API com Echo v5. Agora é a vez do frontend: vamos criar um projeto Vue 3 com Vite dentro do repositório Go, instalar o Vue Router e configurar o proxy de desenvolvimento. Com o proxy, o navegador enxerga frontend e API na mesma origem, sem CORS, que é exatamente o que acontece em produção quando o Go servir o Vue embutido.
Crie o projeto Vue com Vite
O Vite é a ferramenta oficial de build do ecossistema Vue: servidor de desenvolvimento com recarga instantânea e build de produção otimizado. Na raiz do projeto (vueapp/), crie o frontend na pasta web com o template vue:
cd vueapp
npm create vite@latest web -- --template vue --no-interactive --no-immediate
cd web
npm install
npm install vue-router@4
As opções evitam as perguntas interativas: --template vue escolhe Vue com JavaScript, e --no-immediate não sobe o servidor automaticamente. Se preferir TypeScript, use --template vue-ts; o resto da série funciona igual, só com extensões .ts.
No teste desta série, o package.json ficou com:
{
"name": "web",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.42",
"vue-router": "^4.6.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.8",
"vite": "^8.3.0"
}
}
Apague o componente de exemplo, que não vamos usar:
rm -rf src/components src/assets/*.svg
mkdir -p src/views
Proxy do Vite para a API Go
Em desenvolvimento rodam dois servidores: o Vite em localhost:5173 e o Go em 127.0.0.1:8080. Se o navegador chamasse o Go direto, seriam origens diferentes e você teria de configurar CORS, e o cookie de sessão da parte 4 complicaria ainda mais. A saída mais simples é o Vite repassar /api para o Go. Edite web/vite.config.js:
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [vue()],
server: {
// Em desenvolvimento, o Vite (5173) repassa /api para o Go (8080)
proxy: {
'/api': 'http://127.0.0.1:8080',
},
},
})
Assim o navegador só conversa com localhost:5173. Em produção, o Go serve o frontend e a API na mesma porta, e o código Vue não muda nada: sempre chama /api/... com caminho relativo.
Um cliente de API pequeno
Não precisamos de biblioteca HTTP. Crie web/src/api.js com uma função que envia e recebe JSON e transforma respostas de erro em exceção com a mensagem da API:
// Pequeno cliente da API: JSON na ida e na volta, cookie de sessão automático.
export async function api(method, path, body) {
const res = await fetch(`/api${path}`, {
method,
headers: body ? { 'Content-Type': 'application/json' } : {},
body: body ? JSON.stringify(body) : undefined,
})
if (res.status === 204) return null
const data = await res.json().catch(() => ({}))
if (!res.ok) {
const err = new Error(data.message || `HTTP ${res.status}`)
err.status = res.status
throw err
}
return data
}
Como a API está na mesma origem, o fetch envia o cookie de sessão automaticamente; o padrão credentials: 'same-origin' já basta. O Echo devolve os erros no formato {"message": "..."}, então data.message vira o texto exibido na tela.
Rotas do Vue Router
Teremos duas páginas: /login e /tarefas. Crie web/src/router.js. A proteção de rota (meta.auth) já fica declarada aqui e passa a funcionar quando a API de login existir, na parte 4:
import { createRouter, createWebHistory } from 'vue-router'
import { api } from './api'
import LoginView from './views/LoginView.vue'
import TasksView from './views/TasksView.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', redirect: '/tarefas' },
{ path: '/login', component: LoginView },
{ path: '/tarefas', component: TasksView, meta: { auth: true } },
],
})
// Antes de abrir uma página protegida, pergunta ao Go se a sessão vale
router.beforeEach(async (to) => {
if (!to.meta.auth) return true
try {
await api('GET', '/me')
return true
} catch {
return { path: '/login', query: { next: to.fullPath } }
}
})
export default router
O createWebHistory() usa URLs limpas, como /tarefas, sem o #. O preço é que o servidor precisa devolver o index.html quando alguém abre /tarefas direto no navegador. O Vite faz isso sozinho em desenvolvimento; na parte 5 configuramos o Echo para fazer o mesmo.
Ligue o router em web/src/main.js e reduza o App.vue a um contêiner com o <RouterView />:
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
<template>
<main class="container">
<RouterView />
</main>
</template>
Páginas provisórias
As telas completas vêm nas partes 3 e 4. Por enquanto, crie as duas views com uma chamada à rota /api/health da parte 1, para provar que o proxy funciona. web/src/views/TasksView.vue:
<script setup>
import { onMounted, ref } from 'vue'
import { api } from '../api'
const status = ref('...')
onMounted(async () => {
status.value = (await api('GET', '/health')).status
})
</script>
<template>
<section class="card">
<h1>Tarefas</h1>
<p>API: {{ status }}</p>
</section>
</template>
E um web/src/views/LoginView.vue mínimo, para a rota existir:
<template>
<section class="card"><h1>Entrar</h1></section>
</template>
Enquanto a rota /api/me não existe, a guarda do router manda /tarefas para o login. Para testar o proxy nesta etapa, comente temporariamente o meta: { auth: true }.
Rode os dois servidores
Terminal 1, na raiz do projeto:
go run .
Terminal 2, em web/:
npm run dev
VITE v8.3.0 ready in 122 ms
➜ Local: http://localhost:5173/
Abra http://localhost:5173/tarefas: a página mostra API: ok, vindo do Go pelo proxy. Dá para conferir também pelo terminal:
curl http://localhost:5173/api/health
{"status":"ok"}
Repare no endereço: o Vite escuta em localhost, que em muitos sistemas resolve para ::1 (IPv6). Se curl http://127.0.0.1:5173 recusar a conexão, use localhost, ou suba o Vite com npm run dev -- --host 127.0.0.1.
Build de produção
Para ver o que o Go vai embutir no final, rode o build:
npm run build
dist/index.html 0.45 kB │ gzip: 0.29 kB
dist/assets/index-o26dGMpn.css 0.92 kB │ gzip: 0.49 kB
dist/assets/index-U0LYTB29.js 92.56 kB │ gzip: 35.69 kB
✓ built in 59ms
A pasta web/dist tem um index.html e os arquivos com hash no nome, prontos para cache longo. O .gitignore gerado pelo Vite já ignora dist; na parte 5 isso vai importar para o go:embed.
Arquivos desta parte no gist
Cada arquivo abre direto no gist da série. O gist traz a versão final do projeto: main.go e routes.go ainda ganham login na parte 4 e o frontend embutido na parte 5.
vite.config.js(emweb/vite.config.js)api.js(emweb/src/api.js)router.js(emweb/src/router.js)main.js(emweb/src/main.js)App.vue(emweb/src/App.vue)
Próximo passo
Frontend e backend já conversam. Na parte 3 escrevemos a API REST de tarefas no Echo v5, com grupo de rotas, Bind de JSON, validação, códigos HTTP corretos e a tela de tarefas completa no Vue.
Série Vue.js + Go com Echo v5: Parte 1: ambiente e primeira API · Parte 2: frontend Vue com Vite · Parte 3: API REST de tarefas · Parte 4: login com sessão e cookie · Parte 5: embed, binário único e Docker