From 1bb3e23c473263d40e2afadccc2eb434da4cb3dd Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 12:18:57 +0000 Subject: [PATCH] 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. --- cmd/api/main.go | 105 ++++++++++---------- internal/app/app.go | 63 ++++++++---- internal/platform/config/config.go | 2 - internal/testutil/integration_test_utils.go | 27 ++++- 4 files changed, 123 insertions(+), 74 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index d884959..f53e32b 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -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") } \ No newline at end of file diff --git a/internal/app/app.go b/internal/app/app.go index 229a0ab..d89538a 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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, } } \ No newline at end of file diff --git a/internal/platform/config/config.go b/internal/platform/config/config.go index 76b41f4..dd7a8c4 100644 --- a/internal/platform/config/config.go +++ b/internal/platform/config/config.go @@ -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"), diff --git a/internal/testutil/integration_test_utils.go b/internal/testutil/integration_test_utils.go index 48cdd53..a159be2 100644 --- a/internal/testutil/integration_test_utils.go +++ b/internal/testutil/integration_test_utils.go @@ -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{