BuildBot

One Binary, Two Worlds

The dev loop

Lesson 2 of 5

What you'll learn

  • Run the Go API and the Vite dev server side by side
  • Point Vite's server.proxy at the Go port so /api/ requests pass through
  • Understand what CORS actually is, and why the proxy makes it a non-issue in dev

Development runs two processes: go run ./cmd/notesvc serving the API on :8080, and npm run dev in web/ serving the SPA on :5173 with hot module replacement. You get Go's compile-check loop on one side and Vite's instant reload on the other — neither tool has to know how the other works.

The catch: your React code fetches /api/notes, but the browser is looking at localhost:5173, and the API lives on localhost:8080. Vite's dev proxy bridges it. Because lesson 1 put every server route under /api/, the config is one line:

// web/vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      "/api": "http://localhost:8080",
    },
  },
});

Now fetch("/api/notes") hits Vite, Vite forwards it to Go over plain server-to-server HTTP, and the response comes back as if Vite had produced it. The browser only ever talks to one origin.

Hot reload stays fully intact on both sides: edit a component and Vite swaps it in place; edit a handler and restart go run (or let a watcher do it) — the proxy reconnects on the next request.

What CORS actually is

CORS is not a server firewall — it's a browser rule. An origin is the scheme + host + port triple, so http://localhost:5173 and http://localhost:8080 are different origins even on one machine. When page JS fetches across origins, the browser delivers the response to your code only if the server opted in with an Access-Control-Allow-Origin header; for non-simple requests (a JSON POST, an Authorization header) it first sends an OPTIONS preflight asking permission. No opt-in, no data — the fetch rejects.

The proxy dodges all of this by never creating a cross-origin request: the browser sees one origin, and the Vite-to-Go hop is server-to-server, where CORS doesn't exist. You need real CORS headers only when a different origin's pages must call your API — a separately-hosted frontend, or third-party consumers. Our end state (lesson 5) serves SPA and API from one binary on one origin, so we never ship CORS config at all.

CORS errors are a symptom, not the disease

If dev fetches fail with a CORS error, the fix is almost never to sprinkle Access-Control-Allow-Origin: * on the Go server — it's that a request bypassed the proxy. Check that the fetch uses a relative /api/... path, not a hardcoded http://localhost:8080/....

The challenge models both hops: a fetch through the proxy (one origin, no check) and a direct cross-origin fetch (browser demands the header).

Proxy vs cross-origin fetch (JS model)

Run it. The same fetch is tried through the Vite proxy and directly cross-origin. The browser model only releases a cross-origin response when the server sent Access-Control-Allow-Origin — the proxy path never triggers the check.

Loading editor…
Knowledge check

Why does Vite's server.proxy make CORS irrelevant during development?

Next: the client side of the contract — a typed fetch helper, loading and error state, and an optimistic update that can roll itself back.

Saved on this device. Sign in to sync your progress everywhere.