{"id":1641,"date":"2026-09-22T22:42:46","date_gmt":"2026-09-23T01:42:46","guid":{"rendered":"https:\/\/www.linuxpro.com.br\/?p=1641"},"modified":"2026-09-22T22:53:21","modified_gmt":"2026-09-23T01:53:21","slug":"vue-go-echo-v5-parte-2-frontend-vue-com-vite","status":"publish","type":"post","link":"https:\/\/www.linuxpro.com.br\/es\/2026\/09\/vue-go-echo-v5-parte-2-frontend-vue-com-vite\/","title":{"rendered":"Vue.js + Go con Echo v5, parte 2: frontend Vue con Vite"},"content":{"rendered":"<p><img loading=\"lazy\" decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/09\/vue-go-echo-v5-parte-2.webp\" alt=\"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\" width=\"1486\" height=\"856\" \/><\/p>\n<p><strong>S\u00e9rie Vue.js + Go com Echo v5:<\/strong> <a href=\"\/2026\/09\/vue-go-echo-v5-parte-1-ambiente-e-primeira-api\/\">Parte 1: ambiente e primeira API<\/a> \u00b7 <a href=\"\/2026\/09\/vue-go-echo-v5-parte-2-frontend-vue-com-vite\/\">Parte 2: frontend Vue com Vite<\/a> \u00b7 <a href=\"\/2026\/09\/vue-go-echo-v5-parte-3-api-rest-de-tarefas\/\">Parte 3: API REST de tarefas<\/a> \u00b7 <a href=\"\/2026\/09\/vue-go-echo-v5-parte-4-login-com-sessao-e-cookie\/\">Parte 4: login com sess\u00e3o e cookie<\/a> \u00b7 <a href=\"\/2026\/09\/vue-go-echo-v5-parte-5-embed-binario-unico-e-docker\/\">Parte 5: embed, bin\u00e1rio \u00fanico e Docker<\/a><\/p>\n<p><strong>C\u00f3digo completo:<\/strong> todos os arquivos da s\u00e9rie est\u00e3o no <a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08\">gist vue-go-echo-v5<\/a>, com um README que mostra em qual pasta cada um fica.<\/p>\n<p>Na <a href=\"\/2026\/09\/vue-go-echo-v5-parte-1-ambiente-e-primeira-api\/\">parte 1<\/a> subimos a primeira rota da API com Echo v5. Agora \u00e9 a vez do frontend: vamos criar um projeto <strong>Vue 3 com Vite<\/strong> dentro do reposit\u00f3rio Go, instalar o Vue Router e configurar o <strong>proxy de desenvolvimento<\/strong>. Com o proxy, o navegador enxerga frontend e API na mesma origem, sem CORS, que \u00e9 exatamente o que acontece em produ\u00e7\u00e3o quando o Go servir o Vue embutido.<\/p>\n<p><!-- more --><\/p>\n<h2>Crie o projeto Vue com Vite<\/h2>\n<p>O Vite \u00e9 a ferramenta oficial de build do ecossistema Vue: servidor de desenvolvimento com recarga instant\u00e2nea e build de produ\u00e7\u00e3o otimizado. Na raiz do projeto (<code>vueapp\/<\/code>), crie o frontend na pasta <code>web<\/code> com o template <code>vue<\/code>:<\/p>\n<pre><code class=\"language-bash\">cd vueapp\nnpm create vite@latest web -- --template vue --no-interactive --no-immediate\ncd web\nnpm install\nnpm install vue-router@4<\/code><\/pre>\n<p>As op\u00e7\u00f5es evitam as perguntas interativas: <code>--template vue<\/code> escolhe Vue com JavaScript, e <code>--no-immediate<\/code> n\u00e3o sobe o servidor automaticamente. Se preferir TypeScript, use <code>--template vue-ts<\/code>; o resto da s\u00e9rie funciona igual, s\u00f3 com extens\u00f5es <code>.ts<\/code>.<\/p>\n<p>No teste desta s\u00e9rie, o <code>package.json<\/code> ficou com:<\/p>\n<pre><code class=\"language-json\">{\n  \"name\": \"web\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"vue\": \"^3.5.42\",\n    \"vue-router\": \"^4.6.4\"\n  },\n  \"devDependencies\": {\n    \"@vitejs\/plugin-vue\": \"^6.0.8\",\n    \"vite\": \"^8.3.0\"\n  }\n}<\/code><\/pre>\n<p>Apague o componente de exemplo, que n\u00e3o vamos usar:<\/p>\n<pre><code class=\"language-bash\">rm -rf src\/components src\/assets\/*.svg\nmkdir -p src\/views<\/code><\/pre>\n<h2>Proxy do Vite para a API Go<\/h2>\n<p>Em desenvolvimento rodam dois servidores: o Vite em <code>localhost:5173<\/code> e o Go em <code>127.0.0.1:8080<\/code>. Se o navegador chamasse o Go direto, seriam origens diferentes e voc\u00ea teria de configurar CORS, e o cookie de sess\u00e3o da parte 4 complicaria ainda mais. A sa\u00edda mais simples \u00e9 o Vite repassar <code>\/api<\/code> para o Go. Edite <a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-vite-config-js\"><code>web\/vite.config.js<\/code><\/a>:<\/p>\n<pre><code class=\"language-javascript\">import vue from '@vitejs\/plugin-vue'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: [vue()],\n  server: {\n    \/\/ Em desenvolvimento, o Vite (5173) repassa \/api para o Go (8080)\n    proxy: {\n      '\/api': 'http:\/\/127.0.0.1:8080',\n    },\n  },\n})<\/code><\/pre>\n<p>Assim o navegador s\u00f3 conversa com <code>localhost:5173<\/code>. Em produ\u00e7\u00e3o, o Go serve o frontend e a API na mesma porta, e o c\u00f3digo Vue n\u00e3o muda nada: sempre chama <code>\/api\/...<\/code> com caminho relativo.<\/p>\n<h2>Um cliente de API pequeno<\/h2>\n<p>N\u00e3o precisamos de biblioteca HTTP. Crie <a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-api-js\"><code>web\/src\/api.js<\/code><\/a> com uma fun\u00e7\u00e3o que envia e recebe JSON e transforma respostas de erro em exce\u00e7\u00e3o com a mensagem da API:<\/p>\n<pre><code class=\"language-javascript\">\/\/ Pequeno cliente da API: JSON na ida e na volta, cookie de sess\u00e3o autom\u00e1tico.\nexport async function api(method, path, body) {\n  const res = await fetch(`\/api${path}`, {\n    method,\n    headers: body ? { 'Content-Type': 'application\/json' } : {},\n    body: body ? JSON.stringify(body) : undefined,\n  })\n  if (res.status === 204) return null\n  const data = await res.json().catch(() =&gt; ({}))\n  if (!res.ok) {\n    const err = new Error(data.message || `HTTP ${res.status}`)\n    err.status = res.status\n    throw err\n  }\n  return data\n}<\/code><\/pre>\n<p>Como a API est\u00e1 na mesma origem, o <code>fetch<\/code> envia o cookie de sess\u00e3o automaticamente; o padr\u00e3o <code>credentials: 'same-origin'<\/code> j\u00e1 basta. O Echo devolve os erros no formato <code>{\"message\": \"...\"}<\/code>, ent\u00e3o <code>data.message<\/code> vira o texto exibido na tela.<\/p>\n<h2>Rotas do Vue Router<\/h2>\n<p>Teremos duas p\u00e1ginas: <code>\/login<\/code> e <code>\/tarefas<\/code>. Crie <a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-router-js\"><code>web\/src\/router.js<\/code><\/a>. A prote\u00e7\u00e3o de rota (<code>meta.auth<\/code>) j\u00e1 fica declarada aqui e passa a funcionar quando a API de login existir, na parte 4:<\/p>\n<pre><code class=\"language-javascript\">import { createRouter, createWebHistory } from 'vue-router'\nimport { api } from '.\/api'\nimport LoginView from '.\/views\/LoginView.vue'\nimport TasksView from '.\/views\/TasksView.vue'\n\nconst router = createRouter({\n  history: createWebHistory(),\n  routes: [\n    { path: '\/', redirect: '\/tarefas' },\n    { path: '\/login', component: LoginView },\n    { path: '\/tarefas', component: TasksView, meta: { auth: true } },\n  ],\n})\n\n\/\/ Antes de abrir uma p\u00e1gina protegida, pergunta ao Go se a sess\u00e3o vale\nrouter.beforeEach(async (to) =&gt; {\n  if (!to.meta.auth) return true\n  try {\n    await api('GET', '\/me')\n    return true\n  } catch {\n    return { path: '\/login', query: { next: to.fullPath } }\n  }\n})\n\nexport default router<\/code><\/pre>\n<p>O <code>createWebHistory()<\/code> usa URLs limpas, como <code>\/tarefas<\/code>, sem o <code>#<\/code>. O pre\u00e7o \u00e9 que o servidor precisa devolver o <code>index.html<\/code> quando algu\u00e9m abre <code>\/tarefas<\/code> direto no navegador. O Vite faz isso sozinho em desenvolvimento; na parte 5 configuramos o Echo para fazer o mesmo.<\/p>\n<p>Ligue o router em <a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-main-js\"><code>web\/src\/main.js<\/code><\/a> e reduza o <code>App.vue<\/code> a um cont\u00eainer com o <code>&lt;RouterView \/&gt;<\/code>:<\/p>\n<pre><code class=\"language-javascript\">import { createApp } from 'vue'\nimport '.\/style.css'\nimport App from '.\/App.vue'\nimport router from '.\/router'\n\ncreateApp(App).use(router).mount('#app')<\/code><\/pre>\n<pre><code class=\"language-html\">&lt;template&gt;\n  &lt;main class=\"container\"&gt;\n    &lt;RouterView \/&gt;\n  &lt;\/main&gt;\n&lt;\/template&gt;<\/code><\/pre>\n<h2>P\u00e1ginas provis\u00f3rias<\/h2>\n<p>As telas completas v\u00eam nas partes 3 e 4. Por enquanto, crie as duas views com uma chamada \u00e0 rota <code>\/api\/health<\/code> da parte 1, para provar que o proxy funciona. <code>web\/src\/views\/TasksView.vue<\/code>:<\/p>\n<pre><code class=\"language-html\">&lt;script setup&gt;\nimport { onMounted, ref } from 'vue'\nimport { api } from '..\/api'\n\nconst status = ref('...')\nonMounted(async () =&gt; {\n  status.value = (await api('GET', '\/health')).status\n})\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;section class=\"card\"&gt;\n    &lt;h1&gt;Tarefas&lt;\/h1&gt;\n    &lt;p&gt;API: {{ status }}&lt;\/p&gt;\n  &lt;\/section&gt;\n&lt;\/template&gt;<\/code><\/pre>\n<p>E um <code>web\/src\/views\/LoginView.vue<\/code> m\u00ednimo, para a rota existir:<\/p>\n<pre><code class=\"language-html\">&lt;template&gt;\n  &lt;section class=\"card\"&gt;&lt;h1&gt;Entrar&lt;\/h1&gt;&lt;\/section&gt;\n&lt;\/template&gt;<\/code><\/pre>\n<p>Enquanto a rota <code>\/api\/me<\/code> n\u00e3o existe, a guarda do router manda <code>\/tarefas<\/code> para o login. Para testar o proxy nesta etapa, comente temporariamente o <code>meta: { auth: true }<\/code>.<\/p>\n<h2>Rode os dois servidores<\/h2>\n<p>Terminal 1, na raiz do projeto:<\/p>\n<pre><code class=\"language-bash\">go run .<\/code><\/pre>\n<p>Terminal 2, em <code>web\/<\/code>:<\/p>\n<pre><code class=\"language-bash\">npm run dev<\/code><\/pre>\n<pre><code class=\"language-text\">  VITE v8.3.0  ready in 122 ms\n\n  \u279c  Local:   http:\/\/localhost:5173\/<\/code><\/pre>\n<p>Abra <code>http:\/\/localhost:5173\/tarefas<\/code>: a p\u00e1gina mostra <strong>API: ok<\/strong>, vindo do Go pelo proxy. D\u00e1 para conferir tamb\u00e9m pelo terminal:<\/p>\n<pre><code class=\"language-bash\">curl http:\/\/localhost:5173\/api\/health<\/code><\/pre>\n<pre><code class=\"language-text\">{\"status\":\"ok\"}<\/code><\/pre>\n<p>Repare no endere\u00e7o: o Vite escuta em <code>localhost<\/code>, que em muitos sistemas resolve para <code>::1<\/code> (IPv6). Se <code>curl http:\/\/127.0.0.1:5173<\/code> recusar a conex\u00e3o, use <code>localhost<\/code>, ou suba o Vite com <code>npm run dev -- --host 127.0.0.1<\/code>.<\/p>\n<h2>Build de produ\u00e7\u00e3o<\/h2>\n<p>Para ver o que o Go vai embutir no final, rode o build:<\/p>\n<pre><code class=\"language-bash\">npm run build<\/code><\/pre>\n<pre><code class=\"language-text\">dist\/index.html                  0.45 kB \u2502 gzip:  0.29 kB\ndist\/assets\/index-o26dGMpn.css   0.92 kB \u2502 gzip:  0.49 kB\ndist\/assets\/index-U0LYTB29.js   92.56 kB \u2502 gzip: 35.69 kB\n\u2713 built in 59ms<\/code><\/pre>\n<p>A pasta <code>web\/dist<\/code> tem um <code>index.html<\/code> e os arquivos com hash no nome, prontos para cache longo. O <code>.gitignore<\/code> gerado pelo Vite j\u00e1 ignora <code>dist<\/code>; na parte 5 isso vai importar para o <code>go:embed<\/code>.<\/p>\n<h2>Arquivos desta parte no gist<\/h2>\n<p>Cada arquivo abre direto no <a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08\">gist da s\u00e9rie<\/a>. O gist traz a <strong>vers\u00e3o final<\/strong> do projeto: <code>main.go<\/code> e <code>routes.go<\/code> ainda ganham login na parte 4 e o frontend embutido na parte 5.<\/p>\n<ul>\n<li><a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-vite-config-js\"><code>vite.config.js<\/code><\/a> (em <code>web\/vite.config.js<\/code>)<\/li>\n<li><a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-api-js\"><code>api.js<\/code><\/a> (em <code>web\/src\/api.js<\/code>)<\/li>\n<li><a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-router-js\"><code>router.js<\/code><\/a> (em <code>web\/src\/router.js<\/code>)<\/li>\n<li><a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-main-js\"><code>main.js<\/code><\/a> (em <code>web\/src\/main.js<\/code>)<\/li>\n<li><a href=\"https:\/\/gist.github.com\/jniltinho\/c95a0e7bbad4afe779cf143674cbef08#file-app-vue\"><code>App.vue<\/code><\/a> (em <code>web\/src\/App.vue<\/code>)<\/li>\n<\/ul>\n<h2>Pr\u00f3ximo passo<\/h2>\n<p>Frontend e backend j\u00e1 conversam. Na <a href=\"\/2026\/09\/vue-go-echo-v5-parte-3-api-rest-de-tarefas\/\">parte 3<\/a> escrevemos a API REST de tarefas no Echo v5, com grupo de rotas, <code>Bind<\/code> de JSON, valida\u00e7\u00e3o, c\u00f3digos HTTP corretos e a tela de tarefas completa no Vue.<\/p>\n<p><strong>S\u00e9rie Vue.js + Go com Echo v5:<\/strong> <a href=\"\/2026\/09\/vue-go-echo-v5-parte-1-ambiente-e-primeira-api\/\">Parte 1: ambiente e primeira API<\/a> \u00b7 <a href=\"\/2026\/09\/vue-go-echo-v5-parte-2-frontend-vue-com-vite\/\">Parte 2: frontend Vue com Vite<\/a> \u00b7 <a href=\"\/2026\/09\/vue-go-echo-v5-parte-3-api-rest-de-tarefas\/\">Parte 3: API REST de tarefas<\/a> \u00b7 <a href=\"\/2026\/09\/vue-go-echo-v5-parte-4-login-com-sessao-e-cookie\/\">Parte 4: login com sess\u00e3o e cookie<\/a> \u00b7 <a href=\"\/2026\/09\/vue-go-echo-v5-parte-5-embed-binario-unico-e-docker\/\">Parte 5: embed, bin\u00e1rio \u00fanico e Docker<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Crea el frontend Vue 3 con Vite dentro del proyecto Go, configura Vue Router y el proxy de desarrollo para la API de Echo v5.<\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[45,2],"tags":[424,58,31,468],"class_list":["post-1641","post","type-post","status-publish","format-standard","hentry","category-desenv","category-linux","tag-api","tag-go","tag-golang","tag-vue"],"_links":{"self":[{"href":"https:\/\/www.linuxpro.com.br\/es\/wp-json\/wp\/v2\/posts\/1641","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.linuxpro.com.br\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.linuxpro.com.br\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/es\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/es\/wp-json\/wp\/v2\/comments?post=1641"}],"version-history":[{"count":2,"href":"https:\/\/www.linuxpro.com.br\/es\/wp-json\/wp\/v2\/posts\/1641\/revisions"}],"predecessor-version":[{"id":1656,"href":"https:\/\/www.linuxpro.com.br\/es\/wp-json\/wp\/v2\/posts\/1641\/revisions\/1656"}],"wp:attachment":[{"href":"https:\/\/www.linuxpro.com.br\/es\/wp-json\/wp\/v2\/media?parent=1641"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/es\/wp-json\/wp\/v2\/categories?post=1641"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/es\/wp-json\/wp\/v2\/tags?post=1641"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}