Vue.js + Go com Echo v5, parte 2: frontend Vue com Vite

Mascote LinuxPro encaixando um bloco com o logo do Vue num quadro de componentes de interface, com o gopher do Go e o cachorro caramelo cyborg

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.

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