Files
go-simple-api/cmd/api/main.go
T

103 lines
3.5 KiB
Go
Raw Normal View History

2026-07-16 10:13:46 +03:30
// 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")
}