The server — handlers, router, main
Now the server itself — three files, as tabs, in dependency order: handlers.go → router.go → main.go (a handler, then the router that mounts it, then the main that runs the router). Build them top to bottom; main won't compile until the handler and router exist.
1. internal/handlers/handlers.go — the Handlers struct bundles shared dependencies (right now just config + a logger; it gains a database field in lesson 3). Home writes a plain HTML string — lesson 2 swaps it for a Templ template. Health answers /healthz with "ok" — the endpoint Railway will poll in the deploy phase.
2. internal/handlers/router.go — the Chi router and the middleware stack, outermost first:
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger) // one log line per request
r.Use(middleware.Recoverer) // a panic becomes a 500, not a crash
r.Get("/", h.Home)
r.Get("/healthz", h.Health)
Recoverer is the one that saves you at 2am: a panic in any handler becomes a clean 500 instead of taking the whole server down.
3. cmd/signflow/main.go — the entry point. It loads config, builds the router, and serves with graceful shutdown: on Ctrl-C (SIGINT) or SIGTERM it stops accepting new requests and drains in-flight ones for up to 15 seconds before exiting. (cmd/ holds the runnable entrypoint; everything else is a library under internal/.)
Verify. Run it:
go run ./cmd/signflow
You should see structured logs:
level=INFO msg="starting signflow" env=dev port=8080 base_url=http://localhost:8080
level=INFO msg=listening addr=:8080
In another terminal:
$ curl -s localhost:8080/healthz
ok
$ curl -s localhost:8080/
<!doctype html>
<title>SignFlow</title>
<h1>✍️ SignFlow</h1>
<p>The skeleton is up and serving.</p>
Open http://localhost:8080 in a browser — there's your (unstyled) SignFlow. Ctrl-C the server and watch it log shutting down and exit cleanly, not with a stack trace.
Gotcha (port already in use). Start it twice and the second one dies with:
level=ERROR msg="server error" err="listen tcp :8080: bind: address already in use"Something is already on 8080 (often a forgotten
go run). Stop the other process, or run this one on another port:PORT=8081 go run ./cmd/signflow— the config default you just wrote makes that Just Work.