BuildBot

One Binary, Two Worlds

The shape of the app

Lesson 1 of 5

What you'll learn

  • Lay out a repo with a Go module at the root and a Vite React app in web/
  • Draw the boundary at /api/ and design the JSON contract first
  • Decide what belongs to the server (truth) vs the client (presentation)

You shipped a Go service in the last course: routed with the 1.22 ServeMux, handlers as closures over their dependencies, one static binary. Now it needs a real frontend. The shape we're building toward is one repo, one binary, two worlds: Go owns the data and the API, React owns the screen — and by lesson 5 the React build ships inside the Go binary.

The layout keeps both worlds in one module without tangling them:

notesvc/
├── go.mod
├── cmd/notesvc/main.go      # wiring: mux, store, middleware
├── internal/                # handlers, store — server-only code
└── web/                     # the Vite React app (its own package.json)
    ├── package.json
    ├── vite.config.ts
    └── src/

web/ is a normal Vite app — npm create vite@latest web from the repo root. Go tooling ignores it (no .go files), and npm never sees the Go code. The two worlds meet at exactly one place: HTTP.

The boundary is a URL prefix

Every server route lives under /api/. Every other path belongs to the SPA. This one convention does a lot of work — in dev it's what the proxy matches on (lesson 2), and in prod it's how the binary decides JSON vs index.html (lesson 5):

mux := http.NewServeMux()
mux.HandleFunc("GET /api/notes", handleListNotes(store))
mux.HandleFunc("POST /api/notes", handleCreateNote(store))
mux.HandleFunc("GET /api/notes/{id}", handleGetNote(store))

API-first means you design these JSON shapes before writing a component. The contract is the product; both worlds program against it.

Server truth, client presentation

The rule for where code lives: the server owns anything a user could lie about; the client owns anything only that user sees. Validation, IDs, timestamps, ownership, prices — server. Which tab is open, sort order, form drafts, hover state — client. A client can be modified by anyone with DevTools, so client-side validation is a courtesy, never a defense.

Never trust client-supplied fields

If the request body includes id, createdAt, or role, the server must ignore them and compute its own. Decode into a struct that only has the fields a client is allowed to send — Go's decoder simply drops the rest.

The challenge models a request crossing the boundary: the client sends what it may, the server enforces what it must.

Who owns which fields (JS model)

Run it. A polite client and a tampering client both POST a note. The server decodes only the allowed fields, validates, and stamps its own id/createdAt — so the tampered id and role never survive the boundary.

Loading editor…
Knowledge check

Under the API-first rule, where does validation of a new note's title belong?

Next: running both worlds at once — two dev servers, Vite's proxy to the Go port, and why that makes CORS a non-issue in development.

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