Code

The server — handlers, router, main

Now the server itself — three files, as tabs, in dependency order: handlers.gorouter.gomain.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.