From 76893bd114e582c3fe99f36c54f6f37f97c7597b Mon Sep 17 00:00:00 2001 From: David Folch Agulles Date: Wed, 26 Nov 2025 00:21:07 +0100 Subject: [PATCH] Add HTTP layer and server setup - Add strict configuration management (no fallbacks) - Create HTTP router with Chi and CORS middleware - Implement health check endpoint - Add main entry point with database initialization - Server runs on configurable host and port --- cmd/api/main.go | 33 ++++++++++++ go.mod | 9 +++- go.sum | 10 ++++ internal/infrastructure/config/config.go | 66 ++++++++++++++++++++++++ internal/infrastructure/http/router.go | 29 +++++++++++ 5 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 cmd/api/main.go create mode 100644 internal/infrastructure/config/config.go create mode 100644 internal/infrastructure/http/router.go diff --git a/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 0000000..1380a7d --- /dev/null +++ b/cmd/api/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "habit-tracker-api/internal/infrastructure/config" + httpInfra "habit-tracker-api/internal/infrastructure/http" + "habit-tracker-api/internal/infrastructure/persistence/sqlite" +) + +func main() { + cfg, err := config.Load() + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } + + db, err := sqlite.NewDatabase(cfg.DBPath) + if err != nil { + log.Fatalf("Failed to connect to database: %v", err) + } + defer db.Close() + + router := httpInfra.NewRouter(cfg.CORSOrigins) + + addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.Port) + log.Printf("Server starting on %s", addr) + + if err := http.ListenAndServe(addr, router); err != nil { + log.Fatalf("Server failed: %v", err) + } +} diff --git a/go.mod b/go.mod index eec1469..dcb5b13 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,15 @@ module habit-tracker-api -go 1.23.4 +go 1.24.0 + +toolchain go1.24.10 require ( + github.com/go-chi/chi/v5 v5.2.3 // indirect + github.com/go-chi/cors v1.2.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect + golang.org/x/crypto v0.45.0 // indirect ) diff --git a/go.sum b/go.sum index 9e0518a..5364729 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,14 @@ +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= +github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= diff --git a/internal/infrastructure/config/config.go b/internal/infrastructure/config/config.go new file mode 100644 index 0000000..d59b793 --- /dev/null +++ b/internal/infrastructure/config/config.go @@ -0,0 +1,66 @@ +package config + +import ( + "fmt" + "os" + + "github.com/joho/godotenv" +) + +type Config struct { + DBType string + DBPath string + Port string + Host string + JWTSecret string + JWTExpiry string + RefreshTokenExpiry string + CORSOrigins string + DefaultTimezone string +} + +func Load() (*Config, error) { + godotenv.Load() + + cfg := &Config{ + DBType: os.Getenv("DB_TYPE"), + DBPath: os.Getenv("DB_PATH"), + Port: os.Getenv("PORT"), + Host: os.Getenv("HOST"), + JWTSecret: os.Getenv("JWT_SECRET"), + JWTExpiry: os.Getenv("JWT_EXPIRY"), + RefreshTokenExpiry: os.Getenv("REFRESH_TOKEN_EXPIRY"), + CORSOrigins: os.Getenv("CORS_ORIGINS"), + DefaultTimezone: os.Getenv("DEFAULT_TIMEZONE"), + } + + if cfg.DBType == "" { + return nil, fmt.Errorf("DB_TYPE is required") + } + if cfg.DBPath == "" { + return nil, fmt.Errorf("DB_PATH is required") + } + if cfg.Port == "" { + return nil, fmt.Errorf("PORT is required") + } + if cfg.Host == "" { + return nil, fmt.Errorf("HOST is required") + } + if cfg.JWTSecret == "" { + return nil, fmt.Errorf("JWT_SECRET is required") + } + if cfg.JWTExpiry == "" { + return nil, fmt.Errorf("JWT_EXPIRY is required") + } + if cfg.RefreshTokenExpiry == "" { + return nil, fmt.Errorf("REFRESH_TOKEN_EXPIRY is required") + } + if cfg.CORSOrigins == "" { + return nil, fmt.Errorf("CORS_ORIGINS is required") + } + if cfg.DefaultTimezone == "" { + return nil, fmt.Errorf("DEFAULT_TIMEZONE is required") + } + + return cfg, nil +} diff --git a/internal/infrastructure/http/router.go b/internal/infrastructure/http/router.go new file mode 100644 index 0000000..760ab25 --- /dev/null +++ b/internal/infrastructure/http/router.go @@ -0,0 +1,29 @@ +package http + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" +) + +func NewRouter(corsOrigins string) *chi.Mux { + r := chi.NewRouter() + + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + r.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{corsOrigins}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, + AllowCredentials: true, + })) + + r.Get("/api/v1/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"ok"}`)) + }) + + return r +}