// Command api is the entrypoint of the application. Its job is narrow and // deliberate: load configuration, construct every shared dependency // exactly once (logger, database, session manager), build the router, and // run an HTTP server with a graceful shutdown sequence. It contains no // business logic itself - that all lives in the packages it wires // together. package main import ( "context" "net/http" "os" "os/signal" "syscall" "time" "git.hamidsoltani.com/hamid/go-simple-api/internal/config" "git.hamidsoltani.com/hamid/go-simple-api/internal/database" "git.hamidsoltani.com/hamid/go-simple-api/internal/logging" "git.hamidsoltani.com/hamid/go-simple-api/internal/router" "git.hamidsoltani.com/hamid/go-simple-api/internal/session" ) func main() { // 1. Configuration - read once, pass down everywhere as a value. cfg := config.Load() // 2. Logger - built before anything else so even early startup // failures (DB connection, etc.) get logged as structured JSON, // consistent with every other log line the app produces. logger := logging.New() ctx := context.Background() // 3. Database - connect and verify with a real ping before doing // anything else. If this fails, there's no point starting an HTTP // server that can't actually serve any authenticated route. db, err := database.NewMySQL(ctx, cfg) if err != nil { logger.Error("failed to connect to database", "error", err) os.Exit(1) } // Closes the connection pool once main() returns - i.e. after the // graceful shutdown sequence below completes. defer db.Close() logger.Info("connected to database", "host", cfg.DBHost, "db", cfg.DBName) // 4. Migrate - ensure required tables exist. See // internal/database/migrate.go for why this simple approach is a // deliberate shortcut for a learning project. if err := database.Migrate(ctx, db); err != nil { logger.Error("failed to migrate database", "error", err) os.Exit(1) } logger.Info("database migrated") // 5. Sessions - builds the scs.SessionManager backed by Redis. sessions := session.New(cfg) logger.Info("session manager configured", "redis_addr", cfg.RedisAddr) // 6. Router - assembles every handler/middleware, using the shared // dependencies constructed above. r := router.New(logger, db, sessions, cfg) // We build http.Server explicitly (instead of calling // http.ListenAndServe directly) specifically so we can call // srv.Shutdown(ctx) on it later, for a graceful shutdown. srv := &http.Server{ Addr: ":" + cfg.Port, Handler: r, } // ListenAndServe blocks forever (until the server stops or errors), so // it must run in its own goroutine - otherwise the code below that // listens for OS shutdown signals would never get a chance to run. go func() { logger.Info("server starting", "port", cfg.Port) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Error("server error", "error", err) os.Exit(1) } }() // Block the main goroutine until we receive an interrupt (Ctrl+C) or // termination (e.g. `docker stop`) signal from the OS. quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit logger.Info("shutting down gracefully") // Give any in-flight requests up to 5 seconds to finish before we // force-close them. shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { logger.Error("forced shutdown", "error", err) os.Exit(1) } logger.Info("server stopped") }