103 lines
3.5 KiB
Go
103 lines
3.5 KiB
Go
// 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")
|
||
|
|
}
|