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.
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.
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.