refactor(api): Consolidate server setup

This commit refactors the API server startup logic in `cmd/api/main.go` to simplify the application's architecture.

Key changes:
- Consolidates the three separate HTTP servers (GraphQL API, GraphQL Playground, and Prometheus metrics) into a single `http.Server` instance.
- Uses a single `http.ServeMux` to route requests to the appropriate handlers on distinct paths (`/query`, `/playground`, `/metrics`).
- Removes the now-redundant `PlaygroundPort` from the application's configuration.

This change simplifies the server startup and shutdown logic, reduces resource usage, and makes the application's entry point cleaner and easier to maintain.
This commit is contained in:
google-labs-jules[bot] 2025-10-05 12:18:57 +00:00
parent 488bc141de
commit 1bb3e23c47
4 changed files with 123 additions and 74 deletions

View File

@ -112,84 +112,83 @@ func main() {
log.Fatal(err, "Failed to create sentiment provider")
}
// Create platform components
jwtManager := auth.NewJWTManager()
// Create application services
analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider)
// Create application dependencies
deps := app.Dependencies{
WorkRepo: repos.Work,
UserRepo: repos.User,
AuthorRepo: repos.Author,
TranslationRepo: repos.Translation,
CommentRepo: repos.Comment,
LikeRepo: repos.Like,
BookmarkRepo: repos.Bookmark,
CollectionRepo: repos.Collection,
TagRepo: repos.Tag,
CategoryRepo: repos.Category,
BookRepo: repos.Book,
PublisherRepo: repos.Publisher,
SourceRepo: repos.Source,
CopyrightRepo: repos.Copyright,
MonetizationRepo: repos.Monetization,
AnalyticsRepo: repos.Analytics,
AuthRepo: repos.Auth,
LocalizationRepo: repos.Localization,
SearchClient: searchClient,
AnalyticsService: analyticsService,
JWTManager: jwtManager,
}
// Create application
application := app.NewApplication(repos, searchClient, analyticsService)
application := app.NewApplication(deps)
// Create GraphQL server
resolver := &graph.Resolver{
App: application,
}
jwtManager := auth.NewJWTManager()
srv := NewServerWithAuth(resolver, jwtManager, metrics, obsLogger)
graphQLServer := &http.Server{
// Create handlers
apiHandler := NewServerWithAuth(resolver, jwtManager, metrics, obsLogger)
playgroundHandler := playground.Handler("GraphQL Playground", "/query")
metricsHandler := observability.PrometheusHandler(reg)
// Consolidate handlers into a single mux
mux := http.NewServeMux()
mux.Handle("/query", apiHandler)
mux.Handle("/playground", playgroundHandler)
mux.Handle("/metrics", metricsHandler)
// Create a single HTTP server
mainServer := &http.Server{
Addr: config.Cfg.ServerPort,
Handler: srv,
Handler: mux,
}
log.Info(fmt.Sprintf("GraphQL server created successfully on port %s", config.Cfg.ServerPort))
log.Info(fmt.Sprintf("API server listening on port %s", config.Cfg.ServerPort))
// Create GraphQL playground
playgroundHandler := playground.Handler("GraphQL", "/query")
playgroundServer := &http.Server{
Addr: config.Cfg.PlaygroundPort,
Handler: playgroundHandler,
}
log.Info(fmt.Sprintf("GraphQL playground created successfully on port %s", config.Cfg.PlaygroundPort))
// Create metrics server
metricsServer := &http.Server{
Addr: ":9090",
Handler: observability.PrometheusHandler(reg),
}
log.Info("Metrics server created successfully on port :9090")
// Start HTTP servers in goroutines
// Start the main server in a goroutine
go func() {
log.Info(fmt.Sprintf("Starting GraphQL server on port %s", config.Cfg.ServerPort))
if err := graphQLServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err, "Failed to start GraphQL server")
if err := mainServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err, "Failed to start server")
}
}()
go func() {
log.Info(fmt.Sprintf("Starting GraphQL playground on port %s", config.Cfg.PlaygroundPort))
if err := playgroundServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err, "Failed to start GraphQL playground")
}
}()
go func() {
log.Info("Starting metrics server on port :9090")
if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err, "Failed to start metrics server")
}
}()
// Wait for interrupt signal to gracefully shutdown the servers
// Wait for interrupt signal to gracefully shutdown the server
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Info("Shutting down servers...")
log.Info("Shutting down server...")
// Graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := graphQLServer.Shutdown(ctx); err != nil {
log.Error(err, "GraphQL server forced to shutdown")
if err := mainServer.Shutdown(ctx); err != nil {
log.Error(err, "Server forced to shutdown")
}
if err := playgroundServer.Shutdown(ctx); err != nil {
log.Error(err, "GraphQL playground forced to shutdown")
}
if err := metricsServer.Shutdown(ctx); err != nil {
log.Error(err, "Metrics server forced to shutdown")
}
log.Info("All servers shutdown successfully")
log.Info("Server shut down successfully")
}

View File

@ -15,13 +15,41 @@ import (
"tercul/internal/app/user"
"tercul/internal/app/auth"
"tercul/internal/app/work"
"tercul/internal/data/sql"
"tercul/internal/domain"
auth_domain "tercul/internal/domain/auth"
localization_domain "tercul/internal/domain/localization"
"tercul/internal/domain/search"
work_domain "tercul/internal/domain/work"
platform_auth "tercul/internal/platform/auth"
)
import "tercul/internal/app/authz"
// Dependencies holds all external dependencies for the application.
type Dependencies struct {
WorkRepo work_domain.WorkRepository
UserRepo domain.UserRepository
AuthorRepo domain.AuthorRepository
TranslationRepo domain.TranslationRepository
CommentRepo domain.CommentRepository
LikeRepo domain.LikeRepository
BookmarkRepo domain.BookmarkRepository
CollectionRepo domain.CollectionRepository
TagRepo domain.TagRepository
CategoryRepo domain.CategoryRepository
BookRepo domain.BookRepository
PublisherRepo domain.PublisherRepository
SourceRepo domain.SourceRepository
CopyrightRepo domain.CopyrightRepository
MonetizationRepo domain.MonetizationRepository
AnalyticsRepo analytics.Repository
AuthRepo auth_domain.AuthRepository
LocalizationRepo localization_domain.LocalizationRepository
SearchClient search.SearchClient
AnalyticsService analytics.Service
JWTManager platform_auth.JWTManagement
}
// Application is a container for all the application-layer services.
type Application struct {
Author *author.Service
@ -41,22 +69,21 @@ type Application struct {
Analytics analytics.Service
}
func NewApplication(repos *sql.Repositories, searchClient search.SearchClient, analyticsService analytics.Service) *Application {
jwtManager := platform_auth.NewJWTManager()
authzService := authz.NewService(repos.Work, repos.Translation)
authorService := author.NewService(repos.Author)
bookService := book.NewService(repos.Book, authzService)
bookmarkService := bookmark.NewService(repos.Bookmark, analyticsService)
categoryService := category.NewService(repos.Category)
collectionService := collection.NewService(repos.Collection)
commentService := comment.NewService(repos.Comment, authzService, analyticsService)
likeService := like.NewService(repos.Like, analyticsService)
tagService := tag.NewService(repos.Tag)
translationService := translation.NewService(repos.Translation, authzService)
userService := user.NewService(repos.User, authzService)
localizationService := localization.NewService(repos.Localization)
authService := auth.NewService(repos.User, jwtManager)
workService := work.NewService(repos.Work, searchClient, authzService)
func NewApplication(deps Dependencies) *Application {
authzService := authz.NewService(deps.WorkRepo, deps.TranslationRepo)
authorService := author.NewService(deps.AuthorRepo)
bookService := book.NewService(deps.BookRepo, authzService)
bookmarkService := bookmark.NewService(deps.BookmarkRepo, deps.AnalyticsService)
categoryService := category.NewService(deps.CategoryRepo)
collectionService := collection.NewService(deps.CollectionRepo)
commentService := comment.NewService(deps.CommentRepo, authzService, deps.AnalyticsService)
likeService := like.NewService(deps.LikeRepo, deps.AnalyticsService)
tagService := tag.NewService(deps.TagRepo)
translationService := translation.NewService(deps.TranslationRepo, authzService)
userService := user.NewService(deps.UserRepo, authzService)
localizationService := localization.NewService(deps.LocalizationRepo)
authService := auth.NewService(deps.UserRepo, deps.JWTManager)
workService := work.NewService(deps.WorkRepo, deps.SearchClient, authzService)
return &Application{
Author: authorService,
@ -73,6 +100,6 @@ func NewApplication(repos *sql.Repositories, searchClient search.SearchClient, a
Auth: authService,
Authz: authzService,
Work: workService,
Analytics: analyticsService,
Analytics: deps.AnalyticsService,
}
}

View File

@ -31,7 +31,6 @@ type Config struct {
// Application configuration
Port string
ServerPort string
PlaygroundPort string
Environment string
LogLevel string
@ -84,7 +83,6 @@ func LoadConfig() {
// Application configuration
Port: getEnv("PORT", "8080"),
ServerPort: getEnv("SERVER_PORT", "8080"),
PlaygroundPort: getEnv("PLAYGROUND_PORT", "8081"),
Environment: getEnv("ENVIRONMENT", "development"),
LogLevel: getEnv("LOG_LEVEL", "info"),

View File

@ -163,7 +163,32 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
s.T().Fatalf("Failed to create sentiment provider: %v", err)
}
analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider)
s.App = app.NewApplication(repos, searchClient, analyticsService)
jwtManager := platform_auth.NewJWTManager()
deps := app.Dependencies{
WorkRepo: repos.Work,
UserRepo: repos.User,
AuthorRepo: repos.Author,
TranslationRepo: repos.Translation,
CommentRepo: repos.Comment,
LikeRepo: repos.Like,
BookmarkRepo: repos.Bookmark,
CollectionRepo: repos.Collection,
TagRepo: repos.Tag,
CategoryRepo: repos.Category,
BookRepo: repos.Book,
PublisherRepo: repos.Publisher,
SourceRepo: repos.Source,
CopyrightRepo: repos.Copyright,
MonetizationRepo: repos.Monetization,
AnalyticsRepo: repos.Analytics,
AuthRepo: repos.Auth,
LocalizationRepo: repos.Localization,
SearchClient: searchClient,
AnalyticsService: analyticsService,
JWTManager: jwtManager,
}
s.App = app.NewApplication(deps)
// Create a default admin user for tests
adminUser := &domain.User{