package main import ( "context" {{ if .UseGRPC }} "errors" "net" "net/http" {{ end }} "os" "os/signal" "syscall" "github.com/hibiken/asynq" {{ if .UseGRPC }} "github.com/rs/cors" {{ end }} "github.com/rs/zerolog" "github.com/rs/zerolog/log" "golang.org/x/sync/errgroup" "{{ .ModuleName }}/internal/auth" "{{ .ModuleName }}/internal/config" db "{{ .ModuleName }}/internal/db/sqlc" {{ if .UseGRPC }} gapi "{{ .ModuleName }}/internal/handlers/grpc" {{ end }} {{ if not .UseGRPC }} api "{{ .ModuleName }}/internal/handlers/http" {{ end }} "{{ .ModuleName }}/internal/mailer" "{{ .ModuleName }}/internal/service" "{{ .ModuleName }}/internal/worker" {{ if .UseGRPC }} "{{ .ModuleName }}/pb" {{ end }} "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file" {{ if .UseGRPC }} "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/rakyll/statik/fs" "google.golang.org/grpc" "google.golang.org/grpc/reflection" "google.golang.org/protobuf/encoding/protojson" _ "{{ .ModuleName }}/docs/statik" {{ end }} "github.com/jackc/pgx/v5/pgxpool" _ "github.com/jackc/pgx/v5/stdlib" ) var interruptSignals = []os.Signal{ os.Interrupt, syscall.SIGTERM, syscall.SIGINT, } // @title App Template API // @version 1.0 // @description REST API template: accounts (register/login/email verification/refresh tokens) and multi-tenant organizations (members/invites) already wired up. // @termsOfService http://swagger.io/terms/ // @contact.name App Template // @license.name MIT // @license.url https://opensource.org/licenses/MIT // @host localhost:8080 // @BasePath /api/v1 // @schemes http https // @securityDefinitions.apikey BearerAuth // @in header // @name Authorization // @description PASETO access token. Format: "Bearer ". Some endpoints also accept the equivalent value via the "access_token" cookie. func main() { config, err := config.LoadConfig(".") if err != nil { log.Fatal().Err(err).Msg("cannot load config") } if config.Environment == "development" { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) zerolog.SetGlobalLevel(zerolog.DebugLevel) } ctx, cancel := signal.NotifyContext(context.Background(), interruptSignals...) defer cancel() connPool, err := pgxpool.New(ctx, config.DBAddr) if err != nil { log.Fatal().Err(err).Msg("cannot connect to db") } runDBMigration(config.MigrationURL, config.DBAddr) redisOpt := asynq.RedisClientOpt{ Addr: config.RedisAddr, Password: config.RedisPassword, DB: config.RedisDB, } taskDist := worker.NewRedisTaskDistributor(redisOpt) store := db.NewPostgresStore(connPool) tokenMaker, err := auth.NewPasetoMaker(config) if err != nil { log.Fatal().Err(err).Msg("cannot create token maker") } userService := service.NewUserService(store, taskDist) authService := service.NewAuthService(store, tokenMaker, config) orgService := service.NewOrganizationService(store, taskDist) wg, ctx := errgroup.WithContext(ctx) {{ if not .UseGRPC }} services := api.Services{ UserService: userService, AuthService: authService, OrganizationService: orgService, } runGinServer(ctx, wg, config, store, taskDist, tokenMaker, services) {{ else }} runGatewayServer(ctx, wg, config, store, taskDist, tokenMaker, userService, authService, orgService) runGrpcServer(ctx, wg, config, store, taskDist, tokenMaker, userService, authService, orgService) {{ end }} runTaskProcessor(ctx, wg, redisOpt, store, config) err = wg.Wait() if err != nil { log.Fatal().Err(err).Msg("application terminated with error") } } func runTaskProcessor( ctx context.Context, wg *errgroup.Group, redisOpt asynq.RedisClientOpt, store db.Store, config config.Config, ) { mailer := mailer.NewSMTPSender( config.EmailSenderName, config.EmailSender, config.EmailPassword, config.SMTPAddr, ) taskProcessor := worker.NewRedisTaskProcessor(redisOpt, store, mailer, config) log.Info().Msg("start task processor") if err := taskProcessor.Start(); err != nil { log.Fatal().Err(err).Msg("cannot start task processor") } wg.Go(func() error { <-ctx.Done() log.Info().Msg("gracefully shutting down task processor") taskProcessor.Shutdown() log.Info().Msg("task processor shut down") return nil }) } func runDBMigration(migrationURL, dbSource string) { migration, err := migrate.New(migrationURL, dbSource) if err != nil { log.Fatal().Err(err).Msg("cannot create new migrate instance") } if err = migration.Up(); err != nil && err != migrate.ErrNoChange { log.Fatal().Err(err).Msg("failed to run migrate up") } log.Info().Msg("db migrated successfully") } {{ if not .UseGRPC }} func runGinServer( ctx context.Context, wg *errgroup.Group, config config.Config, store db.Store, td worker.TaskDistributor, tokenMaker auth.Maker, services api.Services, ) { s, err := api.NewServer(config, store, td, tokenMaker, services) if err != nil { log.Fatal().Err(err).Msg("cannot create server") } wg.Go(func() error { err = s.Start(config.HTTPServerAddr) if err != nil { log.Fatal().Err(err).Msg("cannot start server:") return err } return nil }) wg.Go(func() error { <-ctx.Done() log.Info().Msg("shutting down server") err := s.Shutdown(ctx) if err != nil { log.Error().Err(err).Msg("cannot shutdown server") return err } log.Info().Msg("server shut down") return nil }) } {{ end }} {{ if .UseGRPC }} func runGrpcServer( ctx context.Context, wg *errgroup.Group, config config.Config, store db.Store, td worker.TaskDistributor, tokenMaker auth.Maker, userService service.UserService, authService service.AuthService, organizationService service.OrganizationService, ) { server, err := gapi.NewServer(config, store, td, tokenMaker, userService, authService, organizationService) if err != nil { log.Fatal().Err(err).Msg("cannot create server:") } grpcLogger := grpc.UnaryInterceptor(gapi.GrpcLogger) grpcServer := grpc.NewServer(grpcLogger) pb.Register{{ .ServiceName }}Server(grpcServer, server) reflection.Register(grpcServer) listener, err := net.Listen("tcp", config.GrpcServerAddr) if err != nil { log.Fatal().Err(err).Msg("cannot create listener") } wg.Go(func() error { log.Info().Msgf("start gRPC server at %s", listener.Addr().String()) err = grpcServer.Serve(listener) if err != nil { if errors.Is(err, grpc.ErrServerStopped) { return nil } log.Error().Err(err).Msg("gRPC server failed to serve") return err } return nil }) wg.Go(func() error { <-ctx.Done() log.Info().Msg("gracefully shutting down gRPC server") grpcServer.GracefulStop() log.Info().Msg("gRPC server shut down") return nil }) } func runGatewayServer( ctx context.Context, wg *errgroup.Group, config config.Config, store db.Store, td worker.TaskDistributor, tokenMaker auth.Maker, userService service.UserService, authService service.AuthService, organizationService service.OrganizationService, ) { server, err := gapi.NewServer(config, store, td, tokenMaker, userService, authService, organizationService) if err != nil { log.Fatal().Err(err).Msg("cannot create server:") } opts := runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{ MarshalOptions: protojson.MarshalOptions{ UseProtoNames: true, }, UnmarshalOptions: protojson.UnmarshalOptions{ DiscardUnknown: true, }, }) grpcMux := runtime.NewServeMux(opts) err = pb.Register{{ .ServiceName }}HandlerServer(ctx, grpcMux, server) if err != nil { log.Fatal().Err(err).Msg("cannot register handler server") } mux := http.NewServeMux() mux.Handle("/", grpcMux) statikFS, err := fs.New() if err != nil { log.Fatal().Err(err).Msg("cannot create statik fs") } swaggerHandler := http.StripPrefix("/swagger/", http.FileServer(statikFS)) mux.Handle("/swagger/", swaggerHandler) c := cors.New( cors.Options{ AllowedOrigins: config.AllowedOrigins, AllowedMethods: []string{ http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodOptions, }, AllowedHeaders: []string{ "Accept", "Authorization", "Content-Type", "X-CSRF-Token", }, AllowCredentials: true, Debug: config.Environment == "development", }, ) handler := c.Handler(gapi.HTTPLogger(mux)) httpServer := &http.Server{ Handler: handler, Addr: config.HTTPServerAddr, } wg.Go(func() error { log.Info().Msgf("start HTTP gateway server at %s", httpServer.Addr) err = httpServer.ListenAndServe() if err != nil { if errors.Is(err, http.ErrServerClosed) { return nil } log.Error().Err(err).Msg("cannot start http gateway server") return err } return nil }) wg.Go(func() error { <-ctx.Done() log.Info().Msg("gracefully shutting down HTTP gateway server") err := httpServer.Shutdown(context.Background()) if err != nil { log.Error().Err(err).Msg("cannot shut down http gateway server") return err } log.Info().Msg("HTTP gateway server shut down") return nil }) } {{ end }}