Every layer from sessions 01โ10 in one running Go server โ 13 files, 842 lines, standard library plus four packages.
11a
One package per concern. Each route's guards are visible in one line of routes.go.
Recover โ SecureHeaders โ CORS.11b
Seven real requests from the end-to-end run. Each stops at exactly one layer โ or reaches the handler.
11c
Handlers return errors. httpx.Handle turns safe ones into their status and everything else into a bare 500.
func Routes(h *user.Handler, origins []string) http.Handler { mux := http.NewServeMux() H := httpx.Handle authn := func(next http.Handler) http.Handler { return auth.RequireAuth(h.Tokens, h.Refresh, next) } owner := func(next http.Handler) http.Handler { return authn(authz.OwnerOrAdmin(authz.PathOwner("id"), next)) } admin := func(p authz.Perm, next http.Handler) http.Handler { return authn(authz.RequirePerm(p, next)) } // one bucket per route: a shared one locks out normal users loginLim := httpx.NewIPLimiter(time.Minute/5, 5).Limit // 5/min signupLim := httpx.NewIPLimiter(time.Minute/5, 5).Limit // 5/min refreshLim := httpx.NewIPLimiter(time.Minute/30, 30).Limit // 30/min // public, rate limited mux.Handle("POST /auth/register", signupLim(H(h.Register))) mux.Handle("POST /auth/login", loginLim(H(h.Login))) mux.Handle("POST /auth/refresh", refreshLim(H(h.RefreshToken))) // authenticated mux.Handle("POST /auth/logout", authn(H(h.Logout))) mux.Handle("GET /users/me", authn(H(h.Me))) // owner or admin mux.Handle("GET /users/{id}", owner(H(h.Get))) mux.Handle("PATCH /users/{id}", owner(H(h.Patch))) // admin only mux.Handle("GET /admin/users", admin(authz.ListUsers, H(h.List))) mux.Handle("PATCH /admin/users/{id}/role", admin(authz.SetRole, H(h.SetRole))) return httpx.Recover(httpx.SecureHeaders(httpx.CORS(origins, mux))) }
// Error is an error that is safe to show the client. type Error struct { Status int Msg string } // Handle is the one place errors become responses. func Handle(h HandlerFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := h(w, r) if err == nil { return } var e *Error if !errors.As(err, &e) { slog.Error("internal", "path", r.URL.Path, "err", err) e = &Error{500, "internal error"} } JSON(w, e.Status, map[string]string{"error": e.Msg}) }) } // Recover turns a panic into a clean 500; the stack goes to the log only. func Recover(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if v := recover(); v != nil { slog.Error("panic", "path", r.URL.Path, "value", v, "stack", string(debug.Stack())) JSON(w, 500, map[string]string{"error": "internal error"}) } }() next.ServeHTTP(w, r) }) }
*httpx.Error): 400, 401, 404, 409, 413, 422, 429. Anything else โ a DB error, a bug โ becomes internal error.Recover is outermost, so a panic anywhere โ even in a middleware โ still gets a clean reply.The first version used one limiter for register, login and refresh. A normal journey โ sign up, log in, refresh โ hit 429 on its 6th request. Each route now has its own bucket, and refresh gets 30/min.
11d
Real TCP server, real HTTP client, Redis protocol via miniredis. Bodies are cut at 110 characters by the test driver.
โโ 1 ยท register & login POST /auth/register asha โ 201 {"id":"2","email":"asha@example.com","name":"Asha","role":"user","created_at":"2026-09-30T18:13:38.087930999Z"โฆ POST /auth/register ravi โ 201 {"id":"3","email":"ravi@example.com","name":"Ravi","role":"user","created_at":"2026-09-30T18:13:38.455595843Z"โฆ POST /auth/register (role:admin) โ 400 {"error":"bad request"} POST /auth/register (bad input) โ 422 {"error":"email: invalid; name: 1-80 chars; password: 8-72 bytes"} POST /auth/login wrong pw โ 401 {"error":"invalid email or password"} POST /auth/login unknown email โ 401 {"error":"invalid email or password"} POST /auth/login asha โ 200 {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsInN1YiI6IjIiLCJleHAiOjE3OTA3OTI5MTksโฆ โโ 2 ยท authentication GET /users/me (no token) โ 401 {"error":"unauthorized"} GET /users/me asha โ 200 {"id":"2","email":"asha@example.com","name":"Asha","role":"user","created_at":"2026-09-30T18:13:38.087930999Z"โฆ โโ 3 ยท ownership & RBAC PATCH /users/2 asha (own) โ 200 {"id":"2","email":"asha@example.com","name":"Asha K","role":"user","created_at":"2026-09-30T18:13:38.087930999โฆ PATCH /users/3 asha (ravi) โ 404 {"error":"not found"} PATCH /users/3 admin โ 200 {"id":"3","email":"ravi@example.com","name":"Ravi S","role":"user","created_at":"2026-09-30T18:13:38.455595843โฆ GET /admin/users asha โ 403 {"error":"forbidden"} GET /admin/users admin โ 200 [{"id":"1","email":"admin@example.com","name":"Admin","role":"admin","created_at":"2026-09-30T18:13:37.7206601โฆ โโ 4 ยท refresh rotation POST /auth/refresh with R1 โ 200 {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsInN1YiI6IjIiLCJleHAiOjE3OTA3OTI5MTksโฆ POST /auth/refresh with R1 again โ 401 {"error":"unauthorized"} POST /auth/refresh with R2 โ 401 {"error":"unauthorized"} โโ 5 ยท logout denies the access token GET /users/me ravi โ 200 {"id":"3","email":"ravi@example.com","name":"Ravi S","role":"user","created_at":"2026-09-30T18:13:38.455595843โฆ POST /auth/logout ravi โ 204 GET /users/me ravi (same token) โ 401 {"error":"unauthorized"} POST /auth/refresh ravi's cookie โ 401 {"error":"unauthorized"} โโ 6 ยท hardening PATCH /users/2 (2 MB body) โ 413 {"error":"body too large"} OPTIONS from evil.example โ 204 headers: nosniff ยท DENY ยท max-age=63072000; includeSubDomains POST /auth/login (6th this minute) โ 429 {"error":"too many requests"} โโ 7 ยท errors never leak internals GET /debug/db โ 500 {"error":"internal error"} GET /debug/panic โ 500 {"error":"internal error"}
level=ERROR msg=internal path=/debug/db err="pq: connection refused to 10.0.3.7:5432 (user api_app)"
level=ERROR msg=panic path=/debug/panic value="assignment to entry in nil map" stack="goroutine 100 [running]:\nruntime/debug.Stack()โฆ
ERROR refusing to start err="REDIS_ADDR is required\nJWT_SECRET must be โฅ 32 bytes"
exit=1
"role":"admin" in the register body is rejected (unknown field) โ and the handler hard-codes "user" anyway.jti denylist.11e
Everything the capstone had to include โ and the file it lives in.
jtiHS256 pinnedcontextOwnerOrAdmin โ 404 for othersHandle maps errors to statusapi_app role from session 10.Recap
json:"-", redacted logs, one error handler, least-privilege roles.