commit 4b397266474167655f73247a5e0b8e1f42e3d986 Author: KS Jannette Date: Mon May 18 21:20:44 2026 -0400 first commit diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..101543f --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +# Go workspace file +go.work + +# Env file +.env + +# IDE +.idea diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c82fdfb --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +.PHONY: * + +help: ## This help dialog. + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2 | "sort -u"}' $(MAKEFILE_LIST) + +run: ## Start the application + docker compose up --build --remove-orphans + +down: ## Stop the application + docker compose down --remove-orphans + +setup-local: copy-config install-dependencies run ## Sets up your local environment + +install-lint: ## Install Go lint + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + +install-dependencies: ## Install go project dependencies + go mod download + go mod vendor + +copy-config: ## Copy config file + cp .env.dist .env + +lint: install-lint ## Run lint + golangci-lint run + +tests: down ## Run tests + go test -v ./... diff --git a/README.md b/README.md new file mode 100644 index 0000000..aff2c9f --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# Go boilerplate project + +### A straightforward Go boilerplate project equipped with essential definitions to kickstart your project. + +## How to run the application +1 - Copy the .env.dist to .env (you can edit the values if you want) + +`$ cp .env.dist .env` + +2 - Run make command, it will show the list of commands we have to make easier to start the application. + +`$ make` + +3 - Start the application + +`$ make setup-local` + +4 - Run the tests + +`$ make tests` + +5 - Run the lint + +`$ make lint` + +6 - Run all the validations (lint + tests) + +`$ make validation` diff --git a/cmd/app/main.go b/cmd/app/main.go new file mode 100644 index 0000000..cc4d365 --- /dev/null +++ b/cmd/app/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + + "github.com/felipecoppola/go-boilerplate/internal/config" + "github.com/felipecoppola/go-boilerplate/internal/infrastructure/os" + + "go.uber.org/zap" +) + +// Globals that are set at build time. +var ( + appName = "" + appVersion = "" +) + +func main() { + defer func() { _ = zap.L().Sync() }() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + logger := zap.L().With( + zap.String("app_name", appName), + zap.String("app_version", appVersion), + ) + + cfg, err := config.NewConfig() + if err != nil { + logger.Error("error creating config: %w", zap.Error(err)) + return + } + + logger.Info("starting app") + os.SignalListener(logger, cancel) + + if err = run(ctx, cfg); err != nil { + logger.Error("command failed: %w", zap.Error(err)) + return + } + + logger.Info("command completed") +} diff --git a/cmd/app/service.go b/cmd/app/service.go new file mode 100644 index 0000000..6899674 --- /dev/null +++ b/cmd/app/service.go @@ -0,0 +1,29 @@ +package main + +import ( + "context" + + "github.com/kjannette/go-http-server/cmd/app/handler/httpjson" + "github.com/kjannnette/go-http-server/internal/config" + "github.com/kjannette/go-http-server/pkg/logger" + + "go.uber.org/zap" + "golang.org/x/sync/errgroup" +) + +func run(ctx context.Context, cfg *config.Config) error { + lg := zap.L().With(zap.String("app_name", appName), zap.String("app_version", appVersion)) + + eg, ctx := errgroup.WithContext(ctx) + app, h := httpjson.New(lg) + + eg.Go(func(l logger.Logger) func() error { + return func() error { + l.Info("starting server") + defer l.Info("terminated server") + return h.RunServer(ctx, cfg, app) + } + }(lg.Named("run_server"))) + + return eg.Wait() +} diff --git a/cmd/handler/httpjson/handler.go b/cmd/handler/httpjson/handler.go new file mode 100644 index 0000000..292a8d3 --- /dev/null +++ b/cmd/handler/httpjson/handler.go @@ -0,0 +1,56 @@ +package httpjson + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/kjannette/go-http-server/internal/config" + "github.com/labstack/echo/v4" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +// Handler has services that are required by the handler. +type Handler struct { + logger *zap.Logger +} + +// New creates a new Handler instance. +func New(logger *zap.Logger) (*echo.Echo, Handler) { + h := Handler{ + logger: logger, + } + + e := echo.New() + h.setupHealthCheckRoutes(e) + + return e, h +} + +// RunServer runs http server and listens until context is canceled. +func (h *Handler) RunServer(ctx context.Context, conf *config.Config, e *echo.Echo) error { + go func() { + <-ctx.Done() + + timeout, cancel := context.WithTimeout(context.Background(), time.Duration(conf.API.ReadTimeout)*time.Second) + defer cancel() + + _ = e.Shutdown(timeout) // try gracefully first + _ = e.Close() // immediate termination + }() + + server := &http.Server{ + Addr: fmt.Sprintf(":%d", conf.API.Port), + ReadTimeout: time.Duration(conf.API.ReadTimeout) * time.Second, + WriteTimeout: time.Duration(conf.API.WriteTimeout) * time.Second, + IdleTimeout: time.Duration(conf.API.IdleTimeout) * time.Second, + } + + if err := e.StartServer(server); !errors.Is(err, http.ErrServerClosed) { + return errors.Wrap(err, "server error") + } + + return nil +} diff --git a/cmd/handler/httpjson/health.go b/cmd/handler/httpjson/health.go new file mode 100644 index 0000000..e5593fb --- /dev/null +++ b/cmd/handler/httpjson/health.go @@ -0,0 +1,15 @@ +package httpjson + +import ( + "net/http" + + "github.com/labstack/echo/v4" +) + +func (h *Handler) setupHealthCheckRoutes(app *echo.Echo) { + app.GET("/health", h.healthCheck) +} + +func (h *Handler) healthCheck(c echo.Context) error { + return c.String(http.StatusOK, "OK") +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..de0797e --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "context" + "github.com/kjannette/go-server/internal/config" + "github.com/kjannette/go-server/internal/infrastructure/os" + "go.uber.org/zap" +) + +var ( + appName = "" + appVarsion = "" +) + +func main() { + defer func() {_ = zap.L().Sync() }() +} diff --git a/cmd/service.go b/cmd/service.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..ee4bdaa --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,29 @@ +package config + +import ( + "github.com/pkg/errors" + + "github.com/kelseyhightower/envconfig" +) + +// Config represents the application configuration. +type Config struct { + API struct { + Port int `envconfig:"API_PORT" default:"3000"` + ReadTimeout int `envconfig:"API_READ_TIMEOUT" default:"7"` + WriteTimeout int `envconfig:"API_WRITE_TIMEOUT" default:"5"` + IdleTimeout int `envconfig:"API_IDLE_TIMEOUT" default:"5"` + Timeout int `envconfig:"API_TIMEOUT" default:"5"` + } +} + +// NewConfig creates a new initialised application Config. +func NewConfig() (*Config, error) { + var config Config + + if err := envconfig.Process("", &config); err != nil { + return nil, errors.Wrap(err, "error parsing config") + } + + return &config, nil +} diff --git a/internal/domain/.gitkeep b/internal/domain/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/infrastructure/os/signal.go b/internal/infrastructure/os/signal.go new file mode 100644 index 0000000..ee4bdaa --- /dev/null +++ b/internal/infrastructure/os/signal.go @@ -0,0 +1,29 @@ +package config + +import ( + "github.com/pkg/errors" + + "github.com/kelseyhightower/envconfig" +) + +// Config represents the application configuration. +type Config struct { + API struct { + Port int `envconfig:"API_PORT" default:"3000"` + ReadTimeout int `envconfig:"API_READ_TIMEOUT" default:"7"` + WriteTimeout int `envconfig:"API_WRITE_TIMEOUT" default:"5"` + IdleTimeout int `envconfig:"API_IDLE_TIMEOUT" default:"5"` + Timeout int `envconfig:"API_TIMEOUT" default:"5"` + } +} + +// NewConfig creates a new initialised application Config. +func NewConfig() (*Config, error) { + var config Config + + if err := envconfig.Process("", &config); err != nil { + return nil, errors.Wrap(err, "error parsing config") + } + + return &config, nil +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 0000000..c68f87e --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,53 @@ +package logger + +import ( + "log" + "os" + "go.uber.org/zap" + "go.uber.go/zap/zapcore" +) + +type Logger interface { + Debug(msg string, fields ...zap.Field) + Info(msg string, fields ...zap.Field) + Warn(msg string, fields ...zap.Field) + Error(msg string, fields ...zap.Field) + With(fields ...zap.Field) *zap.Logger +} + +// Option is a function to provide the Logger creation with extra initialisation options. +type Option func(*zap.Config) + +// init initializes zap's global loggers (i.e. zap.L() and zap.S()). +// Log level is retrieved from environment variable LOG_LEVEL, defaulting to "info" otherwise. +// To activate this facility, add a blank import to this package. +func init() { + level := os.Getenv("LOG_LEVEL") + if level == "" { + level = "info" + } + + initializeZapGlobals(level) +} + +func initializeZapGlobals(level string) { + var zapLevel zapcore.Level + if err := zapLevel.UnmarshalText([]byte(level)); err != nil { + log.Fatalf("failed to parse logger level: %v", err) + } + + log.Printf("setting logging level to '%s'.\n", zapLevel.String()) + + cfg := zap.NewProductionConfig() + cfg.Level = zap.NewAtomicLevelAt(zapLevel) + cfg.OutputPaths = []string{"stdout"} + cfg.ErrorOutputPaths = []string{"stderr"} + cfg.EncoderConfig.EncodeTime = zapcore.RFC3339NanoTimeEncoder + + logger, err := cfg.Build() + if err != nil { + log.Fatalf("failed to initialize logger: %v", err) + } + + zap.ReplaceGlobals(logger) +}