// Command web is the Phase 1 walking-skeleton HTTP server. It loads env, // builds a slog handler, opens a pgxpool, mounts the chi router, and serves // /, /healthz, /demo/time, and /static/* with graceful shutdown on // SIGINT/SIGTERM (CONTEXT D-19). // // No .env parser lives here — `.env` is exported into the process // environment by `just dev`; production injects real env vars (D-15). package main import ( "context" "errors" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "backend/internal/db" "backend/internal/web" ) func main() { env := os.Getenv("ENV") if env == "" { env = "development" } port := os.Getenv("PORT") if port == "" { port = "8080" } dsn := os.Getenv("DATABASE_URL") // Logger first so even fatal-on-missing-DSN paths produce structured // output. Per Pattern 3: JSON in production, text everywhere else. slog.SetDefault(slog.New(web.NewSlogHandler(env, os.Stdout))) if dsn == "" { slog.Error("DATABASE_URL is required but unset") os.Exit(1) } // signal.NotifyContext (Go 1.21+) is the canonical idiom — equivalent // to signal.Notify + a channel but the resulting ctx propagates the // cancellation through to handlers, pgxpool dialing, etc. ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() pool, err := db.NewPool(ctx, dsn) if err != nil { // T-01-12: never log the DSN — only the error type/message. slog.Error("db connect failed", "err", err) os.Exit(1) } router := web.NewRouter(pool, "./static") srv := &http.Server{ Addr: ":" + port, Handler: router, // T-01-10 slow-client mitigation per RESEARCH Security Domain. ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } go func() { slog.Info("listening", "addr", srv.Addr, "env", env) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { slog.Error("server error", "err", err) os.Exit(1) } }() <-ctx.Done() slog.Info("shutting down") shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { slog.Error("shutdown error", "err", err) } // Pitfall 4: close the pool AFTER Shutdown returns, NOT via defer in // main — defer ordering is unreliable on fatal-exit paths. pool.Close() slog.Info("shutdown complete") }